@thegitai/cli 1.0.0-preview.4 → 1.0.0-preview.6
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 +4 -0
- package/dist/bin/ai.js +8 -2
- package/dist/src/api/auth.js +2 -2
- package/dist/src/api/browser-login.js +2 -2
- package/dist/src/api/chat.js +99 -21
- package/dist/src/api/http.js +16 -3
- package/dist/src/api/models.js +2 -2
- package/dist/src/project-index.js +13 -1
- package/dist/src/ui/repl.js +76 -28
- package/dist/src/ui/tui/bridge.js +3 -0
- package/dist/src/ui/tui/build-frame.js +5 -5
- 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 +5 -5
- package/dist/src/markdown-renderer.js +0 -112
package/README.md
CHANGED
|
@@ -34,6 +34,10 @@ always be viewed or resumed from that computer. Continuing it through the
|
|
|
34
34
|
service requires the TheGitAI account used for that session. Local sessions can
|
|
35
35
|
also be listed while signed out or offline.
|
|
36
36
|
|
|
37
|
+
CLI login tokens use a rolling 24-hour inactivity timeout. If one expires during
|
|
38
|
+
any server request, the CLI saves the local session, removes the expired
|
|
39
|
+
credential, and asks you to run `ai login` before resuming.
|
|
40
|
+
|
|
37
41
|
## Visible to-do list
|
|
38
42
|
|
|
39
43
|
For larger multi-step tasks, the agent keeps a compact to-do list on screen so
|
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 { isTransientNetworkError } from '../src/api/http.js';
|
|
7
|
+
import { 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';
|
|
@@ -268,6 +268,12 @@ export async function main() {
|
|
|
268
268
|
printSessionExit(session);
|
|
269
269
|
}
|
|
270
270
|
main().catch((error) => {
|
|
271
|
-
|
|
271
|
+
if (isAuthenticationError(error)) {
|
|
272
|
+
auth.clearCliAuthConfig();
|
|
273
|
+
console.error(chalk.red(`\n✖ Error: ${authenticationErrorMessage(error)}\n`));
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
console.error(chalk.red(`\n✖ Error: ${error.message}\n`));
|
|
277
|
+
}
|
|
272
278
|
process.exit(1);
|
|
273
279
|
});
|
package/dist/src/api/auth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { getClientStateDir } from '../client-state.js';
|
|
4
|
-
import { ServerApiError, authorizedJson, createTraceContext, failureMessage, normalizeServerUrl, readJsonResponse, retryTransient, } from './http.js';
|
|
4
|
+
import { ServerApiError, authorizedJson, createTraceContext, failureCode, failureMessage, normalizeServerUrl, readJsonResponse, retryTransient, } from './http.js';
|
|
5
5
|
export function getAuthConfigPath(env = process.env) {
|
|
6
6
|
const configured = String(env.THEGITAI_AUTH_CONFIG ?? '').trim();
|
|
7
7
|
if (configured) {
|
|
@@ -78,6 +78,6 @@ export async function logoutFromServer({ config, fetchImpl = globalThis.fetch, }
|
|
|
78
78
|
});
|
|
79
79
|
if (!response.ok && response.status !== 401) {
|
|
80
80
|
const data = (await readJsonResponse(response));
|
|
81
|
-
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
|
|
81
|
+
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId, failureCode(data));
|
|
82
82
|
}
|
|
83
83
|
}
|
|
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import http from 'node:http';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import { openUrl } from '../core/open-url.js';
|
|
5
|
-
import { ServerApiError, createTraceContext, failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
|
|
5
|
+
import { ServerApiError, createTraceContext, failureCode, failureMessage, normalizeServerUrl, readJsonResponse, } from './http.js';
|
|
6
6
|
const DEFAULT_WEBSITE_URL = 'https://thegit.ai';
|
|
7
7
|
const DEFAULT_SERVER_URL = 'https://thegit.ai';
|
|
8
8
|
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
@@ -56,7 +56,7 @@ async function exchangeCodeForToken({ serverUrl, code, codeVerifier, fetchImpl,
|
|
|
56
56
|
});
|
|
57
57
|
const data = (await readJsonResponse(response));
|
|
58
58
|
if (!response.ok) {
|
|
59
|
-
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
|
|
59
|
+
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId, failureCode(data));
|
|
60
60
|
}
|
|
61
61
|
const token = String(data?.token ?? '').trim();
|
|
62
62
|
const customer = data?.customer;
|
package/dist/src/api/chat.js
CHANGED
|
@@ -52,6 +52,7 @@ function parseSseBlock(block) {
|
|
|
52
52
|
return { event, data: text };
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
let toolStateSeqCounter = 0;
|
|
55
56
|
function toolStateFromSession(session) {
|
|
56
57
|
return {
|
|
57
58
|
autoYes: session.autoYes,
|
|
@@ -185,6 +186,7 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
|
|
|
185
186
|
toolCallId: event.call.id,
|
|
186
187
|
result,
|
|
187
188
|
toolState: toolStateFromSession(session),
|
|
189
|
+
toolStateSeq: ++toolStateSeqCounter,
|
|
188
190
|
};
|
|
189
191
|
const trace = createTraceContext(traceId);
|
|
190
192
|
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/tool-result`, {
|
|
@@ -203,6 +205,29 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
|
|
|
203
205
|
throw await readErrorResponse(response, trace.traceId);
|
|
204
206
|
}
|
|
205
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
|
+
}
|
|
206
231
|
async function executeAndPostToolResult({ config, projectIndex, session, event, input, fetchImpl, signal, traceId, }) {
|
|
207
232
|
const turnId = String(event?.turnId ?? '').trim();
|
|
208
233
|
if (!turnId || !event?.call?.id || !event.call.name) {
|
|
@@ -211,10 +236,9 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
211
236
|
if (signal?.aborted) {
|
|
212
237
|
throw new TurnCancelledError();
|
|
213
238
|
}
|
|
214
|
-
const previousTurnId = session.turnState.id;
|
|
215
239
|
const serverSessionTurnId = String(event.sessionTurnId ?? '').trim();
|
|
216
240
|
if (serverSessionTurnId) {
|
|
217
|
-
session
|
|
241
|
+
enterServerTurnId(session, serverSessionTurnId);
|
|
218
242
|
if (!session.clientState.safety.checkpoints.some((checkpoint) => checkpoint.turnId === serverSessionTurnId)) {
|
|
219
243
|
createPromptCheckpoint(session.clientState.safety, 'prompt boundary', serverSessionTurnId);
|
|
220
244
|
}
|
|
@@ -237,7 +261,9 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
237
261
|
});
|
|
238
262
|
}
|
|
239
263
|
finally {
|
|
240
|
-
|
|
264
|
+
if (serverSessionTurnId) {
|
|
265
|
+
exitServerTurnId(session);
|
|
266
|
+
}
|
|
241
267
|
}
|
|
242
268
|
}
|
|
243
269
|
async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, traceId, }) {
|
|
@@ -250,6 +276,31 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
250
276
|
const finalResult = {
|
|
251
277
|
current: null,
|
|
252
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
|
+
}
|
|
253
304
|
async function handleEvent(event) {
|
|
254
305
|
if (event.event === 'status') {
|
|
255
306
|
const message = publicStatusMessage(event.data);
|
|
@@ -261,11 +312,26 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
261
312
|
return;
|
|
262
313
|
}
|
|
263
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();
|
|
264
330
|
await executeAndPostToolResult({
|
|
265
331
|
config,
|
|
266
332
|
projectIndex,
|
|
267
333
|
session,
|
|
268
|
-
event:
|
|
334
|
+
event: data,
|
|
269
335
|
input,
|
|
270
336
|
fetchImpl,
|
|
271
337
|
signal,
|
|
@@ -281,10 +347,12 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
281
347
|
return;
|
|
282
348
|
}
|
|
283
349
|
if (event.event === 'result') {
|
|
350
|
+
await drainParallelTools();
|
|
284
351
|
finalResult.current = event.data;
|
|
285
352
|
return;
|
|
286
353
|
}
|
|
287
354
|
if (event.event === 'cancelled' || event.event === 'error') {
|
|
355
|
+
await drainParallelTools().catch(() => { });
|
|
288
356
|
const message = String(event.data?.message ?? 'Server chat failed.');
|
|
289
357
|
if (event.event === 'cancelled') {
|
|
290
358
|
throw new TurnCancelledError(message);
|
|
@@ -292,24 +360,34 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
292
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);
|
|
293
361
|
}
|
|
294
362
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
+
}
|
|
312
382
|
}
|
|
383
|
+
await drainParallelTools();
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
await reader.cancel().catch(() => { });
|
|
387
|
+
throw error;
|
|
388
|
+
}
|
|
389
|
+
finally {
|
|
390
|
+
await drainParallelTools().catch(() => { });
|
|
313
391
|
}
|
|
314
392
|
buffer += decoder.decode();
|
|
315
393
|
const tail = buffer.trim();
|
package/dist/src/api/http.js
CHANGED
|
@@ -6,11 +6,13 @@ export const CLIENT_PLATFORM_HEADER = 'x-thegitai-client-platform';
|
|
|
6
6
|
export class ServerApiError extends Error {
|
|
7
7
|
status;
|
|
8
8
|
traceId;
|
|
9
|
-
|
|
9
|
+
code;
|
|
10
|
+
constructor(message, status, traceId, code = '') {
|
|
10
11
|
super(traceId ? `${message}\nTrace ID: ${traceId}` : message);
|
|
11
12
|
this.name = 'ServerApiError';
|
|
12
13
|
this.status = status;
|
|
13
14
|
this.traceId = traceId;
|
|
15
|
+
this.code = code;
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
export function createTraceId() {
|
|
@@ -96,9 +98,20 @@ export async function readJsonResponse(response) {
|
|
|
96
98
|
export function failureMessage(data, status) {
|
|
97
99
|
return String(data?.error?.message ?? data?.message ?? `Request failed with ${status}`);
|
|
98
100
|
}
|
|
101
|
+
export function failureCode(data) {
|
|
102
|
+
return typeof data?.error?.code === 'string' ? data.error.code : '';
|
|
103
|
+
}
|
|
104
|
+
export function isAuthenticationError(error) {
|
|
105
|
+
return error instanceof ServerApiError && error.status === 401;
|
|
106
|
+
}
|
|
107
|
+
export function authenticationErrorMessage(error) {
|
|
108
|
+
return error.code === 'AUTH_TOKEN_EXPIRED'
|
|
109
|
+
? 'Your login expired after 24 hours of inactivity. Run `ai login` and resume this saved session.'
|
|
110
|
+
: 'Your login is no longer valid. Run `ai login` and resume this saved session.';
|
|
111
|
+
}
|
|
99
112
|
export async function readErrorResponse(response, traceId = response.headers.get(TRACE_ID_HEADER) ?? '') {
|
|
100
113
|
const data = await readJsonResponse(response);
|
|
101
|
-
return new ServerApiError(failureMessage(data, response.status), response.status, traceId);
|
|
114
|
+
return new ServerApiError(failureMessage(data, response.status), response.status, traceId, failureCode(data));
|
|
102
115
|
}
|
|
103
116
|
export async function authorizedJson({ config, path, method = 'GET', body = null, headers = {}, fetchImpl = globalThis.fetch, timeoutMs = REQUEST_TIMEOUT_MS, }) {
|
|
104
117
|
const trace = createTraceContext();
|
|
@@ -115,7 +128,7 @@ export async function authorizedJson({ config, path, method = 'GET', body = null
|
|
|
115
128
|
});
|
|
116
129
|
const data = await readJsonResponse(response);
|
|
117
130
|
if (!response.ok) {
|
|
118
|
-
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
|
|
131
|
+
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId, failureCode(data));
|
|
119
132
|
}
|
|
120
133
|
return data;
|
|
121
134
|
}
|
package/dist/src/api/models.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { getClientStateDir } from '../client-state.js';
|
|
4
|
-
import { REQUEST_TIMEOUT_MS, ServerApiError, createTraceContext, failureMessage, normalizeServerUrl, readJsonResponse, retryTransient, } from './http.js';
|
|
4
|
+
import { REQUEST_TIMEOUT_MS, ServerApiError, createTraceContext, failureCode, failureMessage, normalizeServerUrl, readJsonResponse, retryTransient, } from './http.js';
|
|
5
5
|
function sanitizeModelInfo(raw) {
|
|
6
6
|
if (!raw || typeof raw !== 'object') {
|
|
7
7
|
return null;
|
|
@@ -75,7 +75,7 @@ export async function fetchServerModels({ config, fetchImpl = globalThis.fetch,
|
|
|
75
75
|
});
|
|
76
76
|
const data = (await readJsonResponse(response));
|
|
77
77
|
if (!response.ok) {
|
|
78
|
-
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId);
|
|
78
|
+
throw new ServerApiError(failureMessage(data, response.status), response.status, trace.traceId, failureCode(data));
|
|
79
79
|
}
|
|
80
80
|
const models = Array.isArray(data?.models)
|
|
81
81
|
? data.models.map(sanitizeModelInfo).filter(Boolean)
|
|
@@ -39,10 +39,21 @@ function removeFile(index, relPath) {
|
|
|
39
39
|
index.chunksByFile.delete(relPath);
|
|
40
40
|
index.fileSignatures.delete(relPath);
|
|
41
41
|
}
|
|
42
|
+
function countIndexedChunks(index) {
|
|
43
|
+
return Array.from(index.chunksByFile.values()).reduce((sum, chunks) => sum + chunks.length, 0);
|
|
44
|
+
}
|
|
42
45
|
async function initializeIndex(index) {
|
|
43
46
|
if (index.initialized) {
|
|
44
|
-
return
|
|
47
|
+
return countIndexedChunks(index);
|
|
48
|
+
}
|
|
49
|
+
if (!index._initializing) {
|
|
50
|
+
index._initializing = scanProjectIntoIndex(index).finally(() => {
|
|
51
|
+
index._initializing = null;
|
|
52
|
+
});
|
|
45
53
|
}
|
|
54
|
+
return index._initializing;
|
|
55
|
+
}
|
|
56
|
+
async function scanProjectIntoIndex(index) {
|
|
46
57
|
const files = listProjectFiles(index.rootDir);
|
|
47
58
|
const chunks = await scanFiles(index.rootDir, files);
|
|
48
59
|
index.fileSignatures.clear();
|
|
@@ -106,6 +117,7 @@ export function createIndex({ rootDir, onStatus = null, onContextLog = null, })
|
|
|
106
117
|
return {
|
|
107
118
|
rootDir: path.resolve(rootDir),
|
|
108
119
|
initialized: false,
|
|
120
|
+
_initializing: null,
|
|
109
121
|
fileSignatures: new Map(),
|
|
110
122
|
chunksByFile: new Map(),
|
|
111
123
|
onStatus,
|
package/dist/src/ui/repl.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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';
|
|
@@ -10,6 +11,8 @@ import { clearTodos, listTodos, setTodoSession } from '../todo-list.js';
|
|
|
10
11
|
import { setScratchSession } from '../scratch-dir.js';
|
|
11
12
|
import { cancelActiveCommand } from '../executor.js';
|
|
12
13
|
import { isTurnFailureMarker } from '../turn-failure-marker.js';
|
|
14
|
+
import { clearCliAuthConfig } from '../api/auth.js';
|
|
15
|
+
import { authenticationErrorMessage, isAuthenticationError, } from '../api/http.js';
|
|
13
16
|
import { setCommandOutputHook, withTuiMode } from '../runtime-mode.js';
|
|
14
17
|
import { collectBackgroundJobUiKillMutations, collectBackgroundJobUiOutputMutations, } from '../tool-executor.js';
|
|
15
18
|
import { clearConversation, } from '../session.js';
|
|
@@ -1289,6 +1292,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1289
1292
|
if (process.stdout.isTTY !== true) {
|
|
1290
1293
|
throw new Error('stdout is not a terminal');
|
|
1291
1294
|
}
|
|
1295
|
+
let fatalError = null;
|
|
1292
1296
|
await withTuiMode(async () => {
|
|
1293
1297
|
setBackgroundJobSession(session.sessionId);
|
|
1294
1298
|
setScratchSession(session.sessionId);
|
|
@@ -1303,6 +1307,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1303
1307
|
let cleanupSudoPasswordPrompt = null;
|
|
1304
1308
|
let sudoPasswordBuffer = '';
|
|
1305
1309
|
const bridge = createRatatuiBridge();
|
|
1310
|
+
captureTerminalWrites();
|
|
1306
1311
|
const { handleShellKeyEvent } = await import('./tui/shell-input.js');
|
|
1307
1312
|
let terminalCols = 80;
|
|
1308
1313
|
let terminalRows = 24;
|
|
@@ -1347,7 +1352,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1347
1352
|
store.update((current) => current.status === status ? { ...current, status: 'Ready' } : current);
|
|
1348
1353
|
}, 2500);
|
|
1349
1354
|
};
|
|
1350
|
-
const terminalTitle = createTerminalTitleController(
|
|
1355
|
+
const terminalTitle = createTerminalTitleController({
|
|
1356
|
+
write: (title) => bridge.setTitle(title),
|
|
1357
|
+
});
|
|
1351
1358
|
const syncTerminalTitle = () => {
|
|
1352
1359
|
const state = store.getState();
|
|
1353
1360
|
terminalTitle.sync({
|
|
@@ -1558,14 +1565,6 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1558
1565
|
scheduleLiveFrameRemount();
|
|
1559
1566
|
void remountTui();
|
|
1560
1567
|
};
|
|
1561
|
-
const saveActiveSession = async () => {
|
|
1562
|
-
try {
|
|
1563
|
-
await saveSessionBoth({ serverSessionClient, session });
|
|
1564
|
-
}
|
|
1565
|
-
catch (error) {
|
|
1566
|
-
appendError(`Session save failed: ${error.message}`);
|
|
1567
|
-
}
|
|
1568
|
-
};
|
|
1569
1568
|
const syncShellStateFromSession = () => {
|
|
1570
1569
|
store.update((current) => ({
|
|
1571
1570
|
...current,
|
|
@@ -1653,9 +1652,31 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1653
1652
|
status: 'Exiting...',
|
|
1654
1653
|
}));
|
|
1655
1654
|
void bridge.close().then(() => {
|
|
1655
|
+
releaseTerminalWrites();
|
|
1656
1656
|
resolveDone?.();
|
|
1657
1657
|
});
|
|
1658
1658
|
};
|
|
1659
|
+
const exitForAuthenticationError = (error) => {
|
|
1660
|
+
if (!isAuthenticationError(error))
|
|
1661
|
+
return false;
|
|
1662
|
+
clearCliAuthConfig(session.env);
|
|
1663
|
+
saveSessionState(session);
|
|
1664
|
+
fatalError = new Error(authenticationErrorMessage(error));
|
|
1665
|
+
requestExit();
|
|
1666
|
+
return true;
|
|
1667
|
+
};
|
|
1668
|
+
const saveActiveSession = async () => {
|
|
1669
|
+
try {
|
|
1670
|
+
await saveSessionBoth({ serverSessionClient, session });
|
|
1671
|
+
return true;
|
|
1672
|
+
}
|
|
1673
|
+
catch (error) {
|
|
1674
|
+
if (exitForAuthenticationError(error))
|
|
1675
|
+
return false;
|
|
1676
|
+
appendError(`Session save failed: ${error.message}`);
|
|
1677
|
+
return true;
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1659
1680
|
const openSudoPasswordPrompt = (command, prompt, signal) => new Promise((resolve) => {
|
|
1660
1681
|
if (exiting || signal?.aborted) {
|
|
1661
1682
|
resolve(null);
|
|
@@ -1792,7 +1813,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1792
1813
|
selectedModelId: selected,
|
|
1793
1814
|
serverModels: fresh,
|
|
1794
1815
|
});
|
|
1795
|
-
await saveActiveSession()
|
|
1816
|
+
if (!(await saveActiveSession()))
|
|
1817
|
+
return;
|
|
1796
1818
|
syncShellStateFromSession();
|
|
1797
1819
|
appendStaticEntry({
|
|
1798
1820
|
body: `Switched to ${formatModelLabel(selected, fresh.models)}. Conversation history preserved.`,
|
|
@@ -1911,6 +1933,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1911
1933
|
await switchToModel(selected.id);
|
|
1912
1934
|
}
|
|
1913
1935
|
catch (error) {
|
|
1936
|
+
if (exitForAuthenticationError(error))
|
|
1937
|
+
return;
|
|
1914
1938
|
appendError(error.message);
|
|
1915
1939
|
}
|
|
1916
1940
|
};
|
|
@@ -1994,6 +2018,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1994
2018
|
await remountTui();
|
|
1995
2019
|
}
|
|
1996
2020
|
catch (error) {
|
|
2021
|
+
if (exitForAuthenticationError(error))
|
|
2022
|
+
return;
|
|
1997
2023
|
store.update((next) => ({
|
|
1998
2024
|
...next,
|
|
1999
2025
|
busy: false,
|
|
@@ -2146,14 +2172,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2146
2172
|
});
|
|
2147
2173
|
}
|
|
2148
2174
|
catch (error) {
|
|
2175
|
+
if (exitForAuthenticationError(error))
|
|
2176
|
+
return;
|
|
2149
2177
|
appendError(error.message);
|
|
2150
2178
|
}
|
|
2151
2179
|
finally {
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2180
|
+
if (!exiting) {
|
|
2181
|
+
store.update((current) => ({
|
|
2182
|
+
...current,
|
|
2183
|
+
busy: false,
|
|
2184
|
+
status: 'Ready',
|
|
2185
|
+
}));
|
|
2186
|
+
}
|
|
2157
2187
|
}
|
|
2158
2188
|
return;
|
|
2159
2189
|
}
|
|
@@ -2167,14 +2197,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2167
2197
|
await openResumePicker();
|
|
2168
2198
|
}
|
|
2169
2199
|
catch (error) {
|
|
2200
|
+
if (exitForAuthenticationError(error))
|
|
2201
|
+
return;
|
|
2170
2202
|
appendError(error.message);
|
|
2171
2203
|
}
|
|
2172
2204
|
finally {
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2205
|
+
if (!exiting) {
|
|
2206
|
+
store.update((current) => ({
|
|
2207
|
+
...current,
|
|
2208
|
+
busy: false,
|
|
2209
|
+
status: current.resumePickerOpen ? 'Select a session to resume' : 'Ready',
|
|
2210
|
+
}));
|
|
2211
|
+
}
|
|
2178
2212
|
}
|
|
2179
2213
|
return;
|
|
2180
2214
|
}
|
|
@@ -2194,14 +2228,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2194
2228
|
}
|
|
2195
2229
|
}
|
|
2196
2230
|
catch (error) {
|
|
2231
|
+
if (exitForAuthenticationError(error))
|
|
2232
|
+
return;
|
|
2197
2233
|
appendError(error.message);
|
|
2198
2234
|
}
|
|
2199
2235
|
finally {
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2236
|
+
if (!exiting) {
|
|
2237
|
+
store.update((current) => ({
|
|
2238
|
+
...current,
|
|
2239
|
+
busy: false,
|
|
2240
|
+
status: current.modelPickerOpen ? 'Select a model' : 'Ready',
|
|
2241
|
+
}));
|
|
2242
|
+
}
|
|
2205
2243
|
}
|
|
2206
2244
|
return;
|
|
2207
2245
|
}
|
|
@@ -2210,7 +2248,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2210
2248
|
clearTodos();
|
|
2211
2249
|
syncTodosState();
|
|
2212
2250
|
latestUsageSummary = null;
|
|
2213
|
-
await saveActiveSession()
|
|
2251
|
+
if (!(await saveActiveSession()))
|
|
2252
|
+
return;
|
|
2214
2253
|
store.replaceTranscript([
|
|
2215
2254
|
{
|
|
2216
2255
|
body: 'Conversation cleared.',
|
|
@@ -2231,6 +2270,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2231
2270
|
activeTurnAbort = turnAbort;
|
|
2232
2271
|
lastTurnStartedAt = turnStartedAt;
|
|
2233
2272
|
todosTouchedThisTurn = false;
|
|
2273
|
+
clearTodos();
|
|
2274
|
+
syncTodosState();
|
|
2234
2275
|
const userEntry = {
|
|
2235
2276
|
body: input,
|
|
2236
2277
|
kind: 'user',
|
|
@@ -2267,7 +2308,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2267
2308
|
return;
|
|
2268
2309
|
latestUsageSummary = result.usageSummary ?? null;
|
|
2269
2310
|
const turnEntries = takePendingTurnEntries();
|
|
2270
|
-
await saveActiveSession()
|
|
2311
|
+
if (!(await saveActiveSession()))
|
|
2312
|
+
return;
|
|
2271
2313
|
syncShellStateFromSession();
|
|
2272
2314
|
store.update((current) => ({
|
|
2273
2315
|
...current,
|
|
@@ -2318,6 +2360,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2318
2360
|
return;
|
|
2319
2361
|
}
|
|
2320
2362
|
const cancelled = isTurnCancelledError(error);
|
|
2363
|
+
if (exitForAuthenticationError(error))
|
|
2364
|
+
return;
|
|
2321
2365
|
store.update((current) => ({
|
|
2322
2366
|
...current,
|
|
2323
2367
|
activeTurnInput: '',
|
|
@@ -2342,7 +2386,8 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2342
2386
|
title: 'System',
|
|
2343
2387
|
},
|
|
2344
2388
|
]);
|
|
2345
|
-
await saveActiveSession()
|
|
2389
|
+
if (!(await saveActiveSession()))
|
|
2390
|
+
return;
|
|
2346
2391
|
}
|
|
2347
2392
|
else {
|
|
2348
2393
|
appendStaticEntries(turnEntries);
|
|
@@ -2570,7 +2615,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2570
2615
|
setTodoSession(null);
|
|
2571
2616
|
setBackgroundJobUpdateHook(null);
|
|
2572
2617
|
await bridge.close();
|
|
2618
|
+
releaseTerminalWrites();
|
|
2573
2619
|
setCommandOutputHook(null);
|
|
2574
2620
|
}
|
|
2575
2621
|
});
|
|
2622
|
+
if (fatalError)
|
|
2623
|
+
throw fatalError;
|
|
2576
2624
|
}
|
|
@@ -2,7 +2,7 @@ import { agentModeLabel } from '../../agent-mode.js';
|
|
|
2
2
|
import { truncate } from '../../utils.js';
|
|
3
3
|
import { formatClientTokenUsage } from '../repl.js';
|
|
4
4
|
import { renderFormattedBodyLines, renderPreformattedBodyLines, } from './markdown-render.js';
|
|
5
|
-
import { line, plainLine, span, wrapText } from './text.js';
|
|
5
|
+
import { displayWidth, line, plainLine, sliceToWidth, span, wrapText, } from './text.js';
|
|
6
6
|
const WORKING_CLOCK_ICON = '◷';
|
|
7
7
|
const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
|
|
8
8
|
const TODO_PANEL_MAX_ROWS = 12;
|
|
@@ -133,14 +133,14 @@ function diffLinePrefix(kind) {
|
|
|
133
133
|
return ' ';
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
-
function fitLine(content, maxWidth) {
|
|
136
|
+
export function fitLine(content, maxWidth) {
|
|
137
137
|
if (maxWidth <= 0)
|
|
138
138
|
return '';
|
|
139
|
-
if (content
|
|
139
|
+
if (displayWidth(content) <= maxWidth)
|
|
140
140
|
return content;
|
|
141
141
|
if (maxWidth === 1)
|
|
142
142
|
return '…';
|
|
143
|
-
return `${content
|
|
143
|
+
return `${sliceToWidth(content, maxWidth - 1)}…`;
|
|
144
144
|
}
|
|
145
145
|
export function formatPromptDirectoryLabel(projectRoot, homeDir = process.env.HOME ?? '') {
|
|
146
146
|
const trimmed = String(projectRoot ?? '').trim();
|
|
@@ -573,7 +573,7 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
573
573
|
return lines;
|
|
574
574
|
}
|
|
575
575
|
function lineCharCount(row) {
|
|
576
|
-
return row.spans.reduce((total, item) => total +
|
|
576
|
+
return row.spans.reduce((total, item) => total + displayWidth(item.text), 0);
|
|
577
577
|
}
|
|
578
578
|
function overlayPanelLine(row, width, color) {
|
|
579
579
|
const padding = Math.max(0, width - lineCharCount(row));
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { line, plainLine, span, wrapText } from './text.js';
|
|
2
|
-
const
|
|
3
|
-
|
|
1
|
+
import { displayWidth, line, padToWidth, plainLine, sliceToWidth, span, wrapText, wrapToWidth, } from './text.js';
|
|
2
|
+
const MIN_TABLE_COLUMN_WIDTH = 3;
|
|
3
|
+
function tableRowOverhead(columnCount) {
|
|
4
|
+
return 3 * columnCount + 1;
|
|
5
|
+
}
|
|
4
6
|
function parseInlineSegments(text) {
|
|
5
7
|
const source = String(text ?? '');
|
|
6
8
|
const segments = [];
|
|
@@ -110,45 +112,37 @@ function stripInlineFormattingForWidth(text) {
|
|
|
110
112
|
.replace(/\*\*([^*]+)\*\*/g, '$1');
|
|
111
113
|
}
|
|
112
114
|
function fitTableColumnWidths(columnWidths, maxWidth) {
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
return
|
|
116
|
-
const
|
|
117
|
-
const budget = Math.
|
|
118
|
-
const total =
|
|
119
|
-
if (
|
|
120
|
-
return
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
for (let index = 1; index < widths.length; index++) {
|
|
130
|
-
if (widths[index] - scaled[index] > widths[targetIndex] - scaled[targetIndex]) {
|
|
131
|
-
targetIndex = index;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
scaled[targetIndex]++;
|
|
135
|
-
remaining--;
|
|
115
|
+
const columnCount = columnWidths.length;
|
|
116
|
+
if (columnCount === 0)
|
|
117
|
+
return [];
|
|
118
|
+
const natural = columnWidths.map((width) => Math.max(1, Math.floor(width)));
|
|
119
|
+
const budget = Math.floor(maxWidth) - tableRowOverhead(columnCount);
|
|
120
|
+
const total = natural.reduce((sum, width) => sum + width, 0);
|
|
121
|
+
if (budget >= total)
|
|
122
|
+
return natural;
|
|
123
|
+
const floorWidth = Math.max(1, Math.min(MIN_TABLE_COLUMN_WIDTH, Math.floor(budget / columnCount)));
|
|
124
|
+
const widths = natural.map((width) => Math.min(width, floorWidth));
|
|
125
|
+
let used = widths.reduce((sum, width) => sum + width, 0);
|
|
126
|
+
if (used > budget) {
|
|
127
|
+
const share = Math.max(1, Math.floor(budget / columnCount));
|
|
128
|
+
for (let index = 0; index < columnCount; index++)
|
|
129
|
+
widths[index] = share;
|
|
130
|
+
used = share * columnCount;
|
|
136
131
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
132
|
+
let remaining = budget - used;
|
|
133
|
+
while (remaining > 0) {
|
|
134
|
+
let grew = false;
|
|
135
|
+
for (let index = 0; index < columnCount && remaining > 0; index++) {
|
|
136
|
+
if (widths[index] >= natural[index])
|
|
141
137
|
continue;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
138
|
+
widths[index]++;
|
|
139
|
+
remaining--;
|
|
140
|
+
grew = true;
|
|
145
141
|
}
|
|
146
|
-
if (
|
|
142
|
+
if (!grew)
|
|
147
143
|
break;
|
|
148
|
-
scaled[targetIndex]--;
|
|
149
|
-
remaining++;
|
|
150
144
|
}
|
|
151
|
-
return
|
|
145
|
+
return widths;
|
|
152
146
|
}
|
|
153
147
|
function normalizeMarkdownTableCells(cells, columnCount) {
|
|
154
148
|
return Array.from({ length: columnCount }, (_, index) => cells[index] ?? '');
|
|
@@ -176,7 +170,7 @@ function parseMarkdownTableBlock(lines, startIndex) {
|
|
|
176
170
|
const normalizedHeaders = normalizeMarkdownTableCells(headers, columnCount);
|
|
177
171
|
const columnWidths = normalizedHeaders.map((header, columnIndex) => {
|
|
178
172
|
const values = [header, ...rows.map((row) => row[columnIndex] ?? '')];
|
|
179
|
-
return Math.max(
|
|
173
|
+
return Math.max(MIN_TABLE_COLUMN_WIDTH, ...values.map((value) => displayWidth(stripInlineFormattingForWidth(value))));
|
|
180
174
|
});
|
|
181
175
|
return {
|
|
182
176
|
nextIndex,
|
|
@@ -280,24 +274,33 @@ function wrapInlineToLines(text, width, bodyColor, prefix = '') {
|
|
|
280
274
|
rowWidth = 0;
|
|
281
275
|
};
|
|
282
276
|
const appendSpan = (part) => {
|
|
283
|
-
|
|
284
|
-
const limit = safeWidth - (indent ? prefix
|
|
277
|
+
let current = rows[rows.length - 1];
|
|
278
|
+
const limit = safeWidth - (rows.length === 1 && indent ? displayWidth(prefix) : 0);
|
|
285
279
|
let remaining = part.text;
|
|
286
280
|
while (remaining.length > 0) {
|
|
287
281
|
const room = limit - rowWidth;
|
|
288
282
|
if (room <= 0) {
|
|
289
283
|
startRow();
|
|
284
|
+
current = rows[rows.length - 1];
|
|
290
285
|
continue;
|
|
291
286
|
}
|
|
292
|
-
|
|
287
|
+
const width = displayWidth(remaining);
|
|
288
|
+
if (width <= room) {
|
|
293
289
|
current.push(span(remaining, styleFromSpan(part)));
|
|
294
|
-
rowWidth +=
|
|
290
|
+
rowWidth += width;
|
|
295
291
|
remaining = '';
|
|
296
292
|
break;
|
|
297
293
|
}
|
|
298
|
-
|
|
299
|
-
|
|
294
|
+
const head = sliceToWidth(remaining, room);
|
|
295
|
+
if (displayWidth(head) > room && current.length > 0) {
|
|
296
|
+
startRow();
|
|
297
|
+
current = rows[rows.length - 1];
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
current.push(span(head, styleFromSpan(part)));
|
|
301
|
+
remaining = remaining.slice(head.length);
|
|
300
302
|
startRow();
|
|
303
|
+
current = rows[rows.length - 1];
|
|
301
304
|
}
|
|
302
305
|
};
|
|
303
306
|
for (const segment of segments) {
|
|
@@ -316,36 +319,35 @@ function wrapInlineToLines(text, width, bodyColor, prefix = '') {
|
|
|
316
319
|
return line(...bodySpans);
|
|
317
320
|
});
|
|
318
321
|
}
|
|
319
|
-
function padCell(text, width) {
|
|
320
|
-
const plain = stripInlineFormattingForWidth(text);
|
|
321
|
-
if (plain.length >= width)
|
|
322
|
-
return plain.slice(0, width);
|
|
323
|
-
return plain + ' '.repeat(width - plain.length);
|
|
324
|
-
}
|
|
325
322
|
function renderTableLines(table, width) {
|
|
326
323
|
const columnWidths = fitTableColumnWidths(table.columnWidths, width);
|
|
324
|
+
const border = (left, joint, right) => line(span(left +
|
|
325
|
+
columnWidths.map((colWidth) => '─'.repeat(colWidth + 2)).join(joint) +
|
|
326
|
+
right, { color: 'cyan', dim: true }));
|
|
327
327
|
const renderRow = (cells, bold) => {
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
328
|
+
const wrapped = columnWidths.map((colWidth, index) => wrapToWidth(stripInlineFormattingForWidth(cells[index] ?? ''), colWidth));
|
|
329
|
+
const height = Math.max(1, ...wrapped.map((cellLines) => cellLines.length));
|
|
330
|
+
const rows = [];
|
|
331
|
+
for (let row = 0; row < height; row++) {
|
|
332
|
+
const spans = [span('│', { color: 'cyan' })];
|
|
333
|
+
for (let column = 0; column < columnWidths.length; column++) {
|
|
334
|
+
const text = wrapped[column]?.[row] ?? '';
|
|
335
|
+
spans.push(span(` ${padToWidth(text, columnWidths[column])} `, {
|
|
336
|
+
color: 'cyan',
|
|
337
|
+
bold,
|
|
338
|
+
}));
|
|
339
|
+
spans.push(span('│', { color: 'cyan' }));
|
|
340
|
+
}
|
|
341
|
+
rows.push(line(...spans));
|
|
342
|
+
}
|
|
343
|
+
return rows;
|
|
333
344
|
};
|
|
334
|
-
const separator = line(span('├' +
|
|
335
|
-
columnWidths.map((colWidth) => '─'.repeat(colWidth + 2)).join('┼') +
|
|
336
|
-
'┤', { color: 'cyan', dim: true }));
|
|
337
|
-
const top = line(span('┌' +
|
|
338
|
-
columnWidths.map((colWidth) => '─'.repeat(colWidth + 2)).join('┬') +
|
|
339
|
-
'┐', { color: 'cyan', dim: true }));
|
|
340
|
-
const bottom = line(span('└' +
|
|
341
|
-
columnWidths.map((colWidth) => '─'.repeat(colWidth + 2)).join('┴') +
|
|
342
|
-
'┘', { color: 'cyan', dim: true }));
|
|
343
345
|
return [
|
|
344
|
-
|
|
345
|
-
renderRow(table.headers, true),
|
|
346
|
-
|
|
347
|
-
...table.rows.
|
|
348
|
-
|
|
346
|
+
border('┌', '┬', '┐'),
|
|
347
|
+
...renderRow(table.headers, true),
|
|
348
|
+
border('├', '┼', '┤'),
|
|
349
|
+
...table.rows.flatMap((row) => renderRow(row, false)),
|
|
350
|
+
border('└', '┴', '┘'),
|
|
349
351
|
];
|
|
350
352
|
}
|
|
351
353
|
function getEntryColor(kind) {
|
|
@@ -410,10 +412,7 @@ export function renderFormattedBodyLines(body, width, kind) {
|
|
|
410
412
|
}
|
|
411
413
|
if (formattedLine.kind === 'table' && formattedLine.table) {
|
|
412
414
|
output.push(...renderTableLines(formattedLine.table, bodyWidth).map((tableLine) => {
|
|
413
|
-
const spans = tableLine.spans.map((part) =>
|
|
414
|
-
...part,
|
|
415
|
-
text: ` ${part.text}`,
|
|
416
|
-
}));
|
|
415
|
+
const spans = tableLine.spans.map((part, index) => index === 0 ? { ...part, text: ` ${part.text}` } : part);
|
|
417
416
|
return { spans };
|
|
418
417
|
}));
|
|
419
418
|
continue;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isTuiMode } from '../../runtime-mode.js';
|
|
1
2
|
const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
|
|
2
3
|
const TITLE_BRAND = 'TheGitAI';
|
|
3
4
|
const TITLE_MARK_PREFIX = '❯_';
|
|
@@ -19,6 +20,8 @@ export function formatTerminalTitle(state, spinnerFrame = 0) {
|
|
|
19
20
|
return `${TITLE_MARK_PREFIX}● ${TITLE_BRAND}`;
|
|
20
21
|
}
|
|
21
22
|
export function writeTerminalTitle(title, stream = process.stdout) {
|
|
23
|
+
if (isTuiMode())
|
|
24
|
+
return;
|
|
22
25
|
if (!('isTTY' in stream) || !stream.isTTY)
|
|
23
26
|
return;
|
|
24
27
|
stream.write(`\x1b]0;${title}\x07`);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const MAX_CAPTURED_CHARS = 1_000_000;
|
|
2
|
+
let restore = null;
|
|
3
|
+
let captured = [];
|
|
4
|
+
let capturedChars = 0;
|
|
5
|
+
function chunkToString(chunk, encoding) {
|
|
6
|
+
if (typeof chunk === 'string')
|
|
7
|
+
return chunk;
|
|
8
|
+
if (chunk instanceof Uint8Array) {
|
|
9
|
+
return Buffer.from(chunk).toString(typeof encoding === 'string' ? encoding : 'utf8');
|
|
10
|
+
}
|
|
11
|
+
return String(chunk ?? '');
|
|
12
|
+
}
|
|
13
|
+
export function captureTerminalWrites() {
|
|
14
|
+
if (restore)
|
|
15
|
+
return;
|
|
16
|
+
const streams = [process.stdout, process.stderr];
|
|
17
|
+
const originals = streams.map((stream) => stream.write.bind(stream));
|
|
18
|
+
for (const stream of streams) {
|
|
19
|
+
stream.write = (chunk, encoding, callback) => {
|
|
20
|
+
if (capturedChars < MAX_CAPTURED_CHARS) {
|
|
21
|
+
const text = chunkToString(chunk, encoding);
|
|
22
|
+
captured.push(text);
|
|
23
|
+
capturedChars += text.length;
|
|
24
|
+
}
|
|
25
|
+
const done = typeof encoding === 'function' ? encoding : callback;
|
|
26
|
+
if (typeof done === 'function')
|
|
27
|
+
done();
|
|
28
|
+
return true;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
restore = () => {
|
|
32
|
+
streams.forEach((stream, index) => {
|
|
33
|
+
stream.write = originals[index];
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function releaseTerminalWrites() {
|
|
38
|
+
if (!restore)
|
|
39
|
+
return;
|
|
40
|
+
restore();
|
|
41
|
+
restore = null;
|
|
42
|
+
if (captured.length > 0) {
|
|
43
|
+
const text = captured.join('');
|
|
44
|
+
captured = [];
|
|
45
|
+
capturedChars = 0;
|
|
46
|
+
process.stderr.write(text);
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/src/ui/tui/text.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
const CSI_PATTERN = /\u001B\[[0-9;?]*[ -\/]*[@-~]/g;
|
|
2
|
+
const OSC_PATTERN = /\u001B\][^\u0007\u001B]*(?:\u0007|\u001B\\)?/g;
|
|
3
|
+
const CONTROL_CHAR_PATTERN = /[\u0000-\u0008\u000B-\u001F\u007F]/g;
|
|
4
|
+
export function stripControlCharacters(text) {
|
|
5
|
+
return String(text ?? '')
|
|
6
|
+
.replace(OSC_PATTERN, '')
|
|
7
|
+
.replace(CSI_PATTERN, '')
|
|
8
|
+
.replace(CONTROL_CHAR_PATTERN, '');
|
|
9
|
+
}
|
|
1
10
|
export function span(text, style = {}) {
|
|
2
|
-
return { text, ...style };
|
|
11
|
+
return { text: stripControlCharacters(text), ...style };
|
|
3
12
|
}
|
|
4
13
|
export function line(...spans) {
|
|
5
14
|
return { spans };
|
|
@@ -14,10 +23,11 @@ export function wrapText(text, width) {
|
|
|
14
23
|
const lines = [];
|
|
15
24
|
for (const rawLine of text.split('\n')) {
|
|
16
25
|
let remaining = rawLine;
|
|
17
|
-
while (remaining
|
|
18
|
-
|
|
26
|
+
while (displayWidth(remaining) > safeWidth) {
|
|
27
|
+
const head = sliceToWidth(remaining, safeWidth);
|
|
28
|
+
let breakAt = head.lastIndexOf(' ');
|
|
19
29
|
if (breakAt <= 0)
|
|
20
|
-
breakAt =
|
|
30
|
+
breakAt = head.length;
|
|
21
31
|
lines.push(remaining.slice(0, breakAt).trimEnd());
|
|
22
32
|
remaining = remaining.slice(breakAt).trimStart();
|
|
23
33
|
}
|
|
@@ -28,3 +38,147 @@ export function wrapText(text, width) {
|
|
|
28
38
|
export function joinLines(blocks) {
|
|
29
39
|
return blocks.flat();
|
|
30
40
|
}
|
|
41
|
+
function isZeroWidthCodePoint(codePoint) {
|
|
42
|
+
return (codePoint === 0x200d ||
|
|
43
|
+
(codePoint >= 0x0300 && codePoint <= 0x036f) ||
|
|
44
|
+
(codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
|
|
45
|
+
(codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
|
|
46
|
+
(codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
|
|
47
|
+
(codePoint >= 0xfe00 && codePoint <= 0xfe0f) ||
|
|
48
|
+
(codePoint >= 0xfe20 && codePoint <= 0xfe2f));
|
|
49
|
+
}
|
|
50
|
+
function isWideCodePoint(codePoint) {
|
|
51
|
+
return ((codePoint >= 0x1100 && codePoint <= 0x115f) ||
|
|
52
|
+
codePoint === 0x231a ||
|
|
53
|
+
codePoint === 0x231b ||
|
|
54
|
+
(codePoint >= 0x23e9 && codePoint <= 0x23ec) ||
|
|
55
|
+
codePoint === 0x23f0 ||
|
|
56
|
+
codePoint === 0x23f3 ||
|
|
57
|
+
(codePoint >= 0x25fd && codePoint <= 0x25fe) ||
|
|
58
|
+
(codePoint >= 0x2614 && codePoint <= 0x2615) ||
|
|
59
|
+
(codePoint >= 0x2648 && codePoint <= 0x2653) ||
|
|
60
|
+
codePoint === 0x267f ||
|
|
61
|
+
codePoint === 0x2693 ||
|
|
62
|
+
codePoint === 0x26a1 ||
|
|
63
|
+
(codePoint >= 0x26aa && codePoint <= 0x26ab) ||
|
|
64
|
+
(codePoint >= 0x26bd && codePoint <= 0x26be) ||
|
|
65
|
+
(codePoint >= 0x26c4 && codePoint <= 0x26c5) ||
|
|
66
|
+
codePoint === 0x26ce ||
|
|
67
|
+
codePoint === 0x26d4 ||
|
|
68
|
+
codePoint === 0x26ea ||
|
|
69
|
+
(codePoint >= 0x26f2 && codePoint <= 0x26f3) ||
|
|
70
|
+
codePoint === 0x26f5 ||
|
|
71
|
+
codePoint === 0x26fa ||
|
|
72
|
+
codePoint === 0x26fd ||
|
|
73
|
+
codePoint === 0x2705 ||
|
|
74
|
+
(codePoint >= 0x270a && codePoint <= 0x270b) ||
|
|
75
|
+
codePoint === 0x2728 ||
|
|
76
|
+
codePoint === 0x274c ||
|
|
77
|
+
codePoint === 0x274e ||
|
|
78
|
+
(codePoint >= 0x2753 && codePoint <= 0x2755) ||
|
|
79
|
+
codePoint === 0x2757 ||
|
|
80
|
+
(codePoint >= 0x2795 && codePoint <= 0x2797) ||
|
|
81
|
+
codePoint === 0x27b0 ||
|
|
82
|
+
codePoint === 0x27bf ||
|
|
83
|
+
(codePoint >= 0x2b1b && codePoint <= 0x2b1c) ||
|
|
84
|
+
codePoint === 0x2b50 ||
|
|
85
|
+
codePoint === 0x2b55 ||
|
|
86
|
+
(codePoint >= 0x2e80 && codePoint <= 0x303e) ||
|
|
87
|
+
(codePoint >= 0x3041 && codePoint <= 0x33ff) ||
|
|
88
|
+
(codePoint >= 0x3400 && codePoint <= 0x4dbf) ||
|
|
89
|
+
(codePoint >= 0x4e00 && codePoint <= 0x9fff) ||
|
|
90
|
+
(codePoint >= 0xa000 && codePoint <= 0xa4cf) ||
|
|
91
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
92
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
93
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
94
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
95
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
96
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
|
|
97
|
+
(codePoint >= 0x1f680 && codePoint <= 0x1f6ff) ||
|
|
98
|
+
(codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
|
|
99
|
+
(codePoint >= 0x1fa70 && codePoint <= 0x1faff) ||
|
|
100
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd));
|
|
101
|
+
}
|
|
102
|
+
export function displayWidth(text) {
|
|
103
|
+
const chars = [...String(text ?? '')];
|
|
104
|
+
let width = 0;
|
|
105
|
+
for (let index = 0; index < chars.length; index++) {
|
|
106
|
+
const codePoint = chars[index].codePointAt(0);
|
|
107
|
+
if (isZeroWidthCodePoint(codePoint))
|
|
108
|
+
continue;
|
|
109
|
+
const next = chars[index + 1]?.codePointAt(0);
|
|
110
|
+
if (next === 0xfe0f) {
|
|
111
|
+
width += 2;
|
|
112
|
+
index++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (next === 0xfe0e) {
|
|
116
|
+
width += 1;
|
|
117
|
+
index++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
width += isWideCodePoint(codePoint) ? 2 : 1;
|
|
121
|
+
}
|
|
122
|
+
return width;
|
|
123
|
+
}
|
|
124
|
+
export function sliceToWidth(text, width) {
|
|
125
|
+
const limit = Math.max(1, Math.floor(width));
|
|
126
|
+
const chars = [...String(text ?? '')];
|
|
127
|
+
let out = '';
|
|
128
|
+
let used = 0;
|
|
129
|
+
for (let index = 0; index < chars.length; index++) {
|
|
130
|
+
const char = chars[index];
|
|
131
|
+
const next = chars[index + 1];
|
|
132
|
+
const selector = next === '️' || next === '︎';
|
|
133
|
+
const cluster = selector ? `${char}${next}` : char;
|
|
134
|
+
const clusterWidth = displayWidth(cluster);
|
|
135
|
+
if (used + clusterWidth > limit)
|
|
136
|
+
break;
|
|
137
|
+
out += cluster;
|
|
138
|
+
used += clusterWidth;
|
|
139
|
+
if (selector)
|
|
140
|
+
index++;
|
|
141
|
+
}
|
|
142
|
+
if (!out)
|
|
143
|
+
return chars[0] ?? '';
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
export function padToWidth(text, width) {
|
|
147
|
+
const current = displayWidth(text);
|
|
148
|
+
if (current >= width)
|
|
149
|
+
return text;
|
|
150
|
+
return text + ' '.repeat(width - current);
|
|
151
|
+
}
|
|
152
|
+
export function wrapToWidth(text, width) {
|
|
153
|
+
const limit = Math.max(1, Math.floor(width));
|
|
154
|
+
const out = [];
|
|
155
|
+
for (const rawLine of String(text ?? '').split('\n')) {
|
|
156
|
+
let current = '';
|
|
157
|
+
const flush = () => {
|
|
158
|
+
out.push(current);
|
|
159
|
+
current = '';
|
|
160
|
+
};
|
|
161
|
+
for (const token of rawLine.split(/\s+/).filter(Boolean)) {
|
|
162
|
+
let rest = token;
|
|
163
|
+
while (displayWidth(rest) > limit) {
|
|
164
|
+
if (current)
|
|
165
|
+
flush();
|
|
166
|
+
const head = sliceToWidth(rest, limit);
|
|
167
|
+
out.push(head);
|
|
168
|
+
rest = rest.slice(head.length);
|
|
169
|
+
}
|
|
170
|
+
if (!rest)
|
|
171
|
+
continue;
|
|
172
|
+
const candidate = current ? `${current} ${rest}` : rest;
|
|
173
|
+
if (displayWidth(candidate) > limit) {
|
|
174
|
+
flush();
|
|
175
|
+
current = rest;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
current = candidate;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
flush();
|
|
182
|
+
}
|
|
183
|
+
return out.length > 0 ? out : [''];
|
|
184
|
+
}
|
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.6",
|
|
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.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",
|
|
44
44
|
"@vscode/ripgrep": "1.18.0"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import chalk from './colors.js';
|
|
2
|
-
function renderInline(text) {
|
|
3
|
-
const parts = String(text ?? '').split(/(`[^`]+`)/g);
|
|
4
|
-
return parts
|
|
5
|
-
.map((part) => part.startsWith('`') && part.endsWith('`') && part.length > 1
|
|
6
|
-
? chalk.cyan(part.slice(1, -1))
|
|
7
|
-
: part)
|
|
8
|
-
.join('');
|
|
9
|
-
}
|
|
10
|
-
function isTableSeparator(line) {
|
|
11
|
-
const cells = splitTableRow(line);
|
|
12
|
-
return (cells.length > 1 &&
|
|
13
|
-
cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim())));
|
|
14
|
-
}
|
|
15
|
-
function splitTableRow(line) {
|
|
16
|
-
const trimmed = String(line ?? '').trim();
|
|
17
|
-
if (!trimmed.includes('|'))
|
|
18
|
-
return [];
|
|
19
|
-
return trimmed
|
|
20
|
-
.replace(/^\|/, '')
|
|
21
|
-
.replace(/\|$/, '')
|
|
22
|
-
.split('|')
|
|
23
|
-
.map((cell) => cell.trim());
|
|
24
|
-
}
|
|
25
|
-
function tableWidth(text) {
|
|
26
|
-
return text.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
27
|
-
}
|
|
28
|
-
function padCell(text, width) {
|
|
29
|
-
return `${text}${' '.repeat(Math.max(0, width - tableWidth(text)))}`;
|
|
30
|
-
}
|
|
31
|
-
function renderTable(rows) {
|
|
32
|
-
if (rows.length < 2 || !isTableSeparator(rows[1].join('|')))
|
|
33
|
-
return [];
|
|
34
|
-
const headers = rows[0];
|
|
35
|
-
const body = rows.slice(2).filter((row) => row.length > 0);
|
|
36
|
-
const columnCount = Math.max(headers.length, ...body.map((row) => row.length));
|
|
37
|
-
const normalizedRows = [headers, ...body].map((row) => Array.from({ length: columnCount }, (_, index) => renderInline(row[index] ?? '')));
|
|
38
|
-
const widths = Array.from({ length: columnCount }, (_, index) => Math.max(...normalizedRows.map((row) => tableWidth(row[index] ?? '')), 3));
|
|
39
|
-
const border = `+${widths.map((width) => '-'.repeat(width + 2)).join('+')}+`;
|
|
40
|
-
const renderRow = (row) => `| ${row.map((cell, index) => padCell(cell, widths[index])).join(' | ')} |`;
|
|
41
|
-
return [
|
|
42
|
-
border,
|
|
43
|
-
renderRow(normalizedRows[0].map((cell) => chalk.bold(cell))),
|
|
44
|
-
border,
|
|
45
|
-
...normalizedRows.slice(1).map(renderRow),
|
|
46
|
-
border,
|
|
47
|
-
];
|
|
48
|
-
}
|
|
49
|
-
function readTableBlock(lines, startIndex) {
|
|
50
|
-
if (startIndex + 1 >= lines.length)
|
|
51
|
-
return null;
|
|
52
|
-
const header = splitTableRow(lines[startIndex]);
|
|
53
|
-
const separator = splitTableRow(lines[startIndex + 1]);
|
|
54
|
-
if (header.length < 2 || separator.length < 2 || !isTableSeparator(lines[startIndex + 1])) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
const rows = [header, separator];
|
|
58
|
-
let index = startIndex + 2;
|
|
59
|
-
while (index < lines.length) {
|
|
60
|
-
const row = splitTableRow(lines[index]);
|
|
61
|
-
if (row.length < 2)
|
|
62
|
-
break;
|
|
63
|
-
rows.push(row);
|
|
64
|
-
index += 1;
|
|
65
|
-
}
|
|
66
|
-
const rendered = renderTable(rows);
|
|
67
|
-
return rendered.length ? { rendered, nextIndex: index } : null;
|
|
68
|
-
}
|
|
69
|
-
export function renderMarkdownForTerminal(markdown) {
|
|
70
|
-
const lines = String(markdown ?? '').replace(/\r\n?/g, '\n').split('\n');
|
|
71
|
-
const output = [];
|
|
72
|
-
let inCodeBlock = false;
|
|
73
|
-
for (let index = 0; index < lines.length; index++) {
|
|
74
|
-
const line = lines[index];
|
|
75
|
-
if (/^\s*```/.test(line)) {
|
|
76
|
-
inCodeBlock = !inCodeBlock;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (inCodeBlock) {
|
|
80
|
-
output.push(chalk.dim(` ${line}`));
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
const table = readTableBlock(lines, index);
|
|
84
|
-
if (table) {
|
|
85
|
-
output.push(...table.rendered);
|
|
86
|
-
index = table.nextIndex - 1;
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
const heading = line.match(/^\s{0,3}#{1,6}\s+(.+)$/);
|
|
90
|
-
if (heading) {
|
|
91
|
-
output.push(chalk.bold(renderInline(heading[1].trim())));
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
const bullet = line.match(/^(\s*)[-*]\s+(.+)$/);
|
|
95
|
-
if (bullet) {
|
|
96
|
-
output.push(`${bullet[1]}- ${renderInline(bullet[2].trim())}`);
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
const numbered = line.match(/^(\s*)\d+[.)]\s+(.+)$/);
|
|
100
|
-
if (numbered) {
|
|
101
|
-
output.push(`${numbered[1]}- ${renderInline(numbered[2].trim())}`);
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
const quote = line.match(/^\s*>\s?(.+)$/);
|
|
105
|
-
if (quote) {
|
|
106
|
-
output.push(chalk.dim(`> ${renderInline(quote[1].trim())}`));
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
output.push(renderInline(line));
|
|
110
|
-
}
|
|
111
|
-
return output.join('\n').trimEnd();
|
|
112
|
-
}
|