@thegitai/cli 1.0.0-preview.14 → 1.0.0-preview.16
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/src/api/chat.js +34 -2
- package/dist/src/help-text.js +12 -2
- package/dist/src/ui/repl.js +162 -10
- package/dist/src/ui/tui/bridge.js +7 -0
- package/dist/src/ui/tui/build-frame.js +91 -25
- package/dist/src/ui/tui/markdown-render.js +11 -2
- package/dist/src/ui/tui/shell-input.js +39 -11
- package/package.json +5 -5
package/dist/src/api/chat.js
CHANGED
|
@@ -248,6 +248,26 @@ async function postUserInputResult({ config, turnId, requestId, result, fetchImp
|
|
|
248
248
|
throw await readErrorResponse(response, trace.traceId);
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
|
+
export async function postInterjection({ config, turnId, text, messageId, fetchImpl = globalThis.fetch, traceId, }) {
|
|
252
|
+
const payload = { text, messageId };
|
|
253
|
+
const trace = createTraceContext(traceId);
|
|
254
|
+
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/interject`, {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
headers: {
|
|
257
|
+
authorization: `Bearer ${config.token}`,
|
|
258
|
+
'content-type': 'application/json',
|
|
259
|
+
...trace.headers,
|
|
260
|
+
},
|
|
261
|
+
body: JSON.stringify(payload),
|
|
262
|
+
});
|
|
263
|
+
if (response.status === 410) {
|
|
264
|
+
return 'stale';
|
|
265
|
+
}
|
|
266
|
+
if (!response.ok) {
|
|
267
|
+
throw await readErrorResponse(response, trace.traceId);
|
|
268
|
+
}
|
|
269
|
+
return 'delivered';
|
|
270
|
+
}
|
|
251
271
|
const turnIdOverrides = new WeakMap();
|
|
252
272
|
function enterServerTurnId(session, serverSessionTurnId) {
|
|
253
273
|
const active = turnIdOverrides.get(session);
|
|
@@ -309,7 +329,7 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
309
329
|
}
|
|
310
330
|
}
|
|
311
331
|
}
|
|
312
|
-
async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, traceId, }) {
|
|
332
|
+
async function consumeTurnStream({ response, config, projectIndex, session, input, fetchImpl, signal, traceId, onTurnStart, onInterjectionDelivered, }) {
|
|
313
333
|
if (!response.body) {
|
|
314
334
|
throw new Error('Server returned an empty chat stream.');
|
|
315
335
|
}
|
|
@@ -345,6 +365,16 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
345
365
|
}
|
|
346
366
|
}
|
|
347
367
|
async function handleEvent(event) {
|
|
368
|
+
if (event.event === 'turn-start') {
|
|
369
|
+
const turnId = String(event.data?.turnId ?? '').trim();
|
|
370
|
+
if (turnId)
|
|
371
|
+
onTurnStart?.(turnId);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (event.event === 'interjection-delivered') {
|
|
375
|
+
onInterjectionDelivered?.(event.data);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
348
378
|
if (event.event === 'status') {
|
|
349
379
|
const data = event.data;
|
|
350
380
|
if (data?.phase === 'analyzing_image') {
|
|
@@ -478,7 +508,7 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
478
508
|
}
|
|
479
509
|
return finalResult.current;
|
|
480
510
|
}
|
|
481
|
-
export async function sendServerUserMessage({ config, projectIndex, session, input, imageAttachments = [], fetchImpl = globalThis.fetch, signal, }) {
|
|
511
|
+
export async function sendServerUserMessage({ config, projectIndex, session, input, imageAttachments = [], fetchImpl = globalThis.fetch, signal, onTurnStart, onInterjectionDelivered, }) {
|
|
482
512
|
const autoAttach = autoAttachImages(input, session.rootDir, imageAttachments);
|
|
483
513
|
const requestImageAttachments = autoAttach.attachments.length > 0
|
|
484
514
|
? [...imageAttachments, ...autoAttach.attachments]
|
|
@@ -535,6 +565,8 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
|
|
|
535
565
|
fetchImpl,
|
|
536
566
|
signal,
|
|
537
567
|
traceId: trace.traceId,
|
|
568
|
+
onTurnStart,
|
|
569
|
+
onInterjectionDelivered,
|
|
538
570
|
});
|
|
539
571
|
applySessionSnapshot(session, result.snapshot, { preserveAgentMode: true });
|
|
540
572
|
return {
|
package/dist/src/help-text.js
CHANGED
|
@@ -59,6 +59,13 @@ const HELP_MARKDOWN = [
|
|
|
59
59
|
'- **Enter** sends • **Shift+Tab** cycles modes • **Esc** cancels the turn •',
|
|
60
60
|
' **Ctrl+C** clears the composer or the queued message, and quits once there',
|
|
61
61
|
' is nothing left to clear. These are the same on macOS, Linux, and Windows.',
|
|
62
|
+
'- **While the agent is working**, Enter queues your message and locks the',
|
|
63
|
+
' composer. Press **Enter** again to send it into the running turn: the agent',
|
|
64
|
+
' picks it up at its next step instead of waiting for the turn to finish, and',
|
|
65
|
+
' the Your messages panel tracks it from queued to delivered. **↑** brings it',
|
|
66
|
+
' back for editing and **Esc**/**Ctrl+C** discards it; both unlock the',
|
|
67
|
+
' composer for the next message. Messages with images cannot be sent mid-turn',
|
|
68
|
+
' and are sent with the next prompt instead.',
|
|
62
69
|
`- **Paste** into the composer with your terminal's paste shortcut (\`${PASTE_SHORTCUT}\``,
|
|
63
70
|
' on this system) or by right-clicking the composer.',
|
|
64
71
|
'- **Copy** from the transcript by dragging to select; double-click copies a',
|
|
@@ -85,8 +92,11 @@ const HELP_MARKDOWN = [
|
|
|
85
92
|
'## Safety & approvals',
|
|
86
93
|
'',
|
|
87
94
|
'- TheGitAI asks before running shell commands or applying file edits.',
|
|
88
|
-
'- At each prompt:
|
|
89
|
-
'
|
|
95
|
+
'- At each prompt: **↑/↓** moves between choices, **Enter** confirms the',
|
|
96
|
+
' highlighted one, and **Esc** denies. Deny is selected by default, and the',
|
|
97
|
+
' choices are Approve once, Approve all remaining actions, and Deny.',
|
|
98
|
+
' Single-letter shortcuts were removed on purpose: a prompt can appear',
|
|
99
|
+
' while you are typing, and a stray letter must never approve anything.',
|
|
90
100
|
'- If an approved `sudo` command needs a password, the TUI shows the exact',
|
|
91
101
|
' command and keeps the password masked and local.',
|
|
92
102
|
'- `-y` / `--yes` at startup auto-approves every shell command and file',
|
package/dist/src/ui/repl.js
CHANGED
|
@@ -852,6 +852,13 @@ export function buildTranscriptFromSessionHistory(history) {
|
|
|
852
852
|
}
|
|
853
853
|
continue;
|
|
854
854
|
}
|
|
855
|
+
if (entry.role === 'user' && entry.kind === 'userInterjection') {
|
|
856
|
+
const text = String(entry.userInput ?? '').trim();
|
|
857
|
+
if (text) {
|
|
858
|
+
entries.push({ body: text, kind: 'user', title: 'You · sent mid-turn' });
|
|
859
|
+
}
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
855
862
|
const text = textFromHistoryEntry(entry);
|
|
856
863
|
if ((entry.role === 'model' || entry.role === 'assistant') && text) {
|
|
857
864
|
if (isTurnFailureMarker(text)) {
|
|
@@ -1022,6 +1029,7 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
1022
1029
|
resumePickerSessions: [],
|
|
1023
1030
|
transcriptScrollOffset: 0,
|
|
1024
1031
|
queuedMessage: null,
|
|
1032
|
+
turnMessages: [],
|
|
1025
1033
|
serverModels: serverModels.models,
|
|
1026
1034
|
sudoPrompt: null,
|
|
1027
1035
|
status: 'Ready',
|
|
@@ -1282,16 +1290,6 @@ export function getApprovalChoiceForCursor(cursor) {
|
|
|
1282
1290
|
return (APPROVAL_OPTIONS[Math.min(Math.max(cursor, 0), APPROVAL_OPTIONS.length - 1)]
|
|
1283
1291
|
?.value ?? 'n');
|
|
1284
1292
|
}
|
|
1285
|
-
export function resolveApprovalChoiceFromInput(input) {
|
|
1286
|
-
const normalizedInput = String(input ?? '').trim().toLowerCase();
|
|
1287
|
-
if (normalizedInput === 'y')
|
|
1288
|
-
return 'y';
|
|
1289
|
-
if (normalizedInput === 'a')
|
|
1290
|
-
return 'a';
|
|
1291
|
-
if (normalizedInput === 'n')
|
|
1292
|
-
return 'n';
|
|
1293
|
-
return null;
|
|
1294
|
-
}
|
|
1295
1293
|
export function pauseBusyClock(state, nowMs) {
|
|
1296
1294
|
if (state.busySince === null || state.busyPausedAt !== null)
|
|
1297
1295
|
return state;
|
|
@@ -1393,6 +1391,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1393
1391
|
let latestUsageSummary = null;
|
|
1394
1392
|
let pendingTurnEntries = [];
|
|
1395
1393
|
let activeTurnAbort = null;
|
|
1394
|
+
let activeServerTurnId = null;
|
|
1395
|
+
let unacknowledged = new Map();
|
|
1396
|
+
const blockedRows = new WeakMap();
|
|
1396
1397
|
let newConversationInFlight = false;
|
|
1397
1398
|
let todosTouchedThisTurn = false;
|
|
1398
1399
|
const syncTodosState = () => {
|
|
@@ -1677,6 +1678,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1677
1678
|
status: 'Ready',
|
|
1678
1679
|
thinkingTitle: '',
|
|
1679
1680
|
thinkingNotes: [],
|
|
1681
|
+
turnMessages: [],
|
|
1680
1682
|
workingTools: [],
|
|
1681
1683
|
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
1682
1684
|
}));
|
|
@@ -1936,6 +1938,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1936
1938
|
store.update((current) => ({
|
|
1937
1939
|
...pauseBusyClock(current, Date.now()),
|
|
1938
1940
|
approvalCursor: getDefaultApprovalCursor(),
|
|
1941
|
+
approvalOpenedAt: Date.now(),
|
|
1939
1942
|
approvalScrollOffset: 0,
|
|
1940
1943
|
approvalPrompt: {
|
|
1941
1944
|
title,
|
|
@@ -2230,6 +2233,129 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2230
2233
|
}));
|
|
2231
2234
|
await handleSubmit(queued.body);
|
|
2232
2235
|
};
|
|
2236
|
+
let turnMessageCounter = 0;
|
|
2237
|
+
const upsertTurnMessage = (id, patch) => {
|
|
2238
|
+
store.update((current) => ({
|
|
2239
|
+
...current,
|
|
2240
|
+
turnMessages: current.turnMessages.map((message) => message.id === id ? { ...message, ...patch } : message),
|
|
2241
|
+
}));
|
|
2242
|
+
};
|
|
2243
|
+
const recoverUnacknowledgedMessages = (autoSubmits = true) => {
|
|
2244
|
+
if (unacknowledged.size === 0)
|
|
2245
|
+
return;
|
|
2246
|
+
const stranded = [...unacknowledged.values()];
|
|
2247
|
+
unacknowledged = new Map();
|
|
2248
|
+
for (const { row } of stranded) {
|
|
2249
|
+
upsertTurnMessage(row, {
|
|
2250
|
+
state: 'queued',
|
|
2251
|
+
note: autoSubmits
|
|
2252
|
+
? 'not read — sending as the next prompt'
|
|
2253
|
+
: 'not read — left in the composer',
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
const existing = store.getState().queuedMessage;
|
|
2257
|
+
const combined = [
|
|
2258
|
+
...stranded.map((entry) => entry.queued),
|
|
2259
|
+
...(existing ? [existing] : []),
|
|
2260
|
+
];
|
|
2261
|
+
const body = combined
|
|
2262
|
+
.map((entry) => entry.body)
|
|
2263
|
+
.filter((entry) => entry.trim())
|
|
2264
|
+
.join('\n\n');
|
|
2265
|
+
if (!body.trim())
|
|
2266
|
+
return;
|
|
2267
|
+
scheduleLiveFrameRemount();
|
|
2268
|
+
store.update((current) => ({
|
|
2269
|
+
...current,
|
|
2270
|
+
queuedMessage: {
|
|
2271
|
+
body,
|
|
2272
|
+
imageAttachments: combined.flatMap((entry) => entry.imageAttachments),
|
|
2273
|
+
pastedChunks: combined.flatMap((entry) => entry.pastedChunks),
|
|
2274
|
+
},
|
|
2275
|
+
}));
|
|
2276
|
+
};
|
|
2277
|
+
const fireQueuedMessage = async () => {
|
|
2278
|
+
const state = store.getState();
|
|
2279
|
+
const queued = state.queuedMessage;
|
|
2280
|
+
if (!queued || !state.busy || exiting)
|
|
2281
|
+
return;
|
|
2282
|
+
const text = (queued.pastedChunks.length
|
|
2283
|
+
? expandPastedChunks(queued.body, queued.pastedChunks)
|
|
2284
|
+
: queued.body).trim();
|
|
2285
|
+
if (!text)
|
|
2286
|
+
return;
|
|
2287
|
+
const turnId = activeServerTurnId;
|
|
2288
|
+
const blockedReason = queued.imageAttachments.length > 0
|
|
2289
|
+
? 'waiting — images go with the next prompt'
|
|
2290
|
+
: !turnId
|
|
2291
|
+
? 'waiting for the turn to end'
|
|
2292
|
+
: null;
|
|
2293
|
+
const existingRow = blockedRows.get(queued);
|
|
2294
|
+
if (existingRow !== undefined) {
|
|
2295
|
+
upsertTurnMessage(existingRow, {
|
|
2296
|
+
state: 'queued',
|
|
2297
|
+
...(blockedReason ? { note: blockedReason } : {}),
|
|
2298
|
+
});
|
|
2299
|
+
if (blockedReason || !turnId)
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
const id = existingRow ?? ++turnMessageCounter;
|
|
2303
|
+
const messageId = `m${id}_${Date.now().toString(36)}`;
|
|
2304
|
+
if (existingRow === undefined) {
|
|
2305
|
+
scheduleLiveFrameRemount();
|
|
2306
|
+
store.update((current) => ({
|
|
2307
|
+
...current,
|
|
2308
|
+
turnMessages: [
|
|
2309
|
+
...current.turnMessages,
|
|
2310
|
+
{
|
|
2311
|
+
id,
|
|
2312
|
+
text,
|
|
2313
|
+
state: blockedReason ? 'queued' : 'sending',
|
|
2314
|
+
...(blockedReason ? { note: blockedReason } : {}),
|
|
2315
|
+
},
|
|
2316
|
+
],
|
|
2317
|
+
}));
|
|
2318
|
+
}
|
|
2319
|
+
if (blockedReason || !turnId) {
|
|
2320
|
+
blockedRows.set(queued, id);
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
blockedRows.delete(queued);
|
|
2324
|
+
unacknowledged.set(messageId, { row: id, queued });
|
|
2325
|
+
scheduleLiveFrameRemount();
|
|
2326
|
+
store.update((current) => current.queuedMessage === queued
|
|
2327
|
+
? { ...current, queuedMessage: null }
|
|
2328
|
+
: current);
|
|
2329
|
+
queueTurnEntry({
|
|
2330
|
+
body: text,
|
|
2331
|
+
kind: 'user',
|
|
2332
|
+
preformatted: queued.pastedChunks.length > 0,
|
|
2333
|
+
title: 'You · sent mid-turn',
|
|
2334
|
+
});
|
|
2335
|
+
const rollback = (note) => {
|
|
2336
|
+
if (!unacknowledged.delete(messageId))
|
|
2337
|
+
return;
|
|
2338
|
+
upsertTurnMessage(id, { state: 'queued', note });
|
|
2339
|
+
scheduleLiveFrameRemount();
|
|
2340
|
+
store.update((current) => current.queuedMessage ? current : { ...current, queuedMessage: queued });
|
|
2341
|
+
};
|
|
2342
|
+
let outcome;
|
|
2343
|
+
try {
|
|
2344
|
+
outcome = await chat.postInterjection({
|
|
2345
|
+
config: authConfig,
|
|
2346
|
+
turnId,
|
|
2347
|
+
text,
|
|
2348
|
+
messageId,
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
catch (error) {
|
|
2352
|
+
rollback(`not sent — ${error?.message ?? 'request failed'}`);
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
if (outcome === 'stale') {
|
|
2356
|
+
rollback('turn ended — sending as the next prompt');
|
|
2357
|
+
}
|
|
2358
|
+
};
|
|
2233
2359
|
const handleSubmit = async (rawInput) => {
|
|
2234
2360
|
const chunks = store.getState().pastedChunks;
|
|
2235
2361
|
const expanded = chunks.length
|
|
@@ -2481,6 +2607,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2481
2607
|
}
|
|
2482
2608
|
latestUsageSummary = null;
|
|
2483
2609
|
disarmExitConfirm();
|
|
2610
|
+
unacknowledged = new Map();
|
|
2484
2611
|
const turnStartedAt = Date.now();
|
|
2485
2612
|
const turnGeneration = ++activeTurnGeneration;
|
|
2486
2613
|
const turnAbort = new AbortController();
|
|
@@ -2513,6 +2640,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2513
2640
|
transcriptScrollOffset: 0,
|
|
2514
2641
|
tokenUsage: formatClientTokenUsage(0, latestUsageSummary),
|
|
2515
2642
|
turnCounter: current.turnCounter + 1,
|
|
2643
|
+
turnMessages: [],
|
|
2516
2644
|
workingTools: [],
|
|
2517
2645
|
}));
|
|
2518
2646
|
try {
|
|
@@ -2523,6 +2651,25 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2523
2651
|
input,
|
|
2524
2652
|
imageAttachments,
|
|
2525
2653
|
signal: turnAbort.signal,
|
|
2654
|
+
onTurnStart: (serverTurnId) => {
|
|
2655
|
+
if (turnGeneration !== activeTurnGeneration)
|
|
2656
|
+
return;
|
|
2657
|
+
activeServerTurnId = serverTurnId;
|
|
2658
|
+
},
|
|
2659
|
+
onInterjectionDelivered: (event) => {
|
|
2660
|
+
if (turnGeneration !== activeTurnGeneration)
|
|
2661
|
+
return;
|
|
2662
|
+
for (const messageId of event.messageIds ?? []) {
|
|
2663
|
+
const pending = unacknowledged.get(messageId);
|
|
2664
|
+
if (!pending)
|
|
2665
|
+
continue;
|
|
2666
|
+
unacknowledged.delete(messageId);
|
|
2667
|
+
upsertTurnMessage(pending.row, {
|
|
2668
|
+
state: 'delivered',
|
|
2669
|
+
note: undefined,
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
},
|
|
2526
2673
|
});
|
|
2527
2674
|
if (turnGeneration !== activeTurnGeneration)
|
|
2528
2675
|
return;
|
|
@@ -2571,6 +2718,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2571
2718
|
status: result.waitingForApproval ? 'Awaiting approval' : 'Ready',
|
|
2572
2719
|
tokenUsage: formatClientTokenUsage(Date.now() - turnStartedAt, latestUsageSummary),
|
|
2573
2720
|
}));
|
|
2721
|
+
recoverUnacknowledgedMessages();
|
|
2574
2722
|
await flushQueuedMessage();
|
|
2575
2723
|
}
|
|
2576
2724
|
catch (error) {
|
|
@@ -2616,6 +2764,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2616
2764
|
appendError(error.message);
|
|
2617
2765
|
}
|
|
2618
2766
|
await remountTui();
|
|
2767
|
+
recoverUnacknowledgedMessages(!cancelled);
|
|
2619
2768
|
if (!cancelled && turnGeneration === activeTurnGeneration) {
|
|
2620
2769
|
await flushQueuedMessage();
|
|
2621
2770
|
}
|
|
@@ -2626,7 +2775,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2626
2775
|
}
|
|
2627
2776
|
if (turnGeneration === activeTurnGeneration) {
|
|
2628
2777
|
lastTurnStartedAt = null;
|
|
2778
|
+
activeServerTurnId = null;
|
|
2629
2779
|
}
|
|
2780
|
+
recoverUnacknowledgedMessages();
|
|
2630
2781
|
}
|
|
2631
2782
|
};
|
|
2632
2783
|
session.onImageAnalysis = (activeImageCount) => {
|
|
@@ -2791,6 +2942,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2791
2942
|
onJobsPickerKill: handleJobsPickerKill,
|
|
2792
2943
|
onSudoPasswordInput: handleSudoPasswordInput,
|
|
2793
2944
|
onSubmit: handleSubmit,
|
|
2945
|
+
onFireQueuedMessage: fireQueuedMessage,
|
|
2794
2946
|
};
|
|
2795
2947
|
const unsubscribe = store.subscribe(() => {
|
|
2796
2948
|
renderCurrentFrame();
|
|
@@ -61,6 +61,13 @@ function normalizeChildMessage(raw) {
|
|
|
61
61
|
deltaLines: Number(raw.deltaLines ?? raw.delta_lines ?? 0),
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
+
if (raw.kind === 'transcriptScrollTo') {
|
|
65
|
+
return {
|
|
66
|
+
op: 'event',
|
|
67
|
+
kind: 'transcriptScrollTo',
|
|
68
|
+
offset: Number(raw.offset ?? 0),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
64
71
|
if (raw.kind !== 'key') {
|
|
65
72
|
return null;
|
|
66
73
|
}
|
|
@@ -2,7 +2,7 @@ import { agentModeLabel } from '../../agent-mode.js';
|
|
|
2
2
|
import { singleLinePreview, truncate } from '../../utils.js';
|
|
3
3
|
import { formatClientTokenUsage } from '../repl.js';
|
|
4
4
|
import { renderFormattedBodyLines, renderPreformattedBodyLines, } from './markdown-render.js';
|
|
5
|
-
import { displayWidth, line, plainLine, sliceToWidth, span, wrapText, } from './text.js';
|
|
5
|
+
import { displayWidth, line, padToWidth, plainLine, sliceToWidth, span, wrapText, } from './text.js';
|
|
6
6
|
import { buildUserInputOverlayLines, } from './user-input.js';
|
|
7
7
|
const WORKING_CLOCK_ICON = '◷';
|
|
8
8
|
const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
|
|
@@ -286,6 +286,46 @@ function todoItemLine(item, width, progressTick = null) {
|
|
|
286
286
|
}
|
|
287
287
|
return line(span(' ○ ', { color: 'gray' }), span(text));
|
|
288
288
|
}
|
|
289
|
+
const TURN_MESSAGE_PANEL_MAX_ROWS = 6;
|
|
290
|
+
const TURN_MESSAGE_LABEL = {
|
|
291
|
+
queued: 'queued',
|
|
292
|
+
sending: 'Processing . . .',
|
|
293
|
+
delivered: 'delivered',
|
|
294
|
+
};
|
|
295
|
+
function turnMessageLine(message, width, progressTick) {
|
|
296
|
+
const DECORATION = 6;
|
|
297
|
+
const budget = Math.max(1, width - DECORATION);
|
|
298
|
+
const preferredLabel = Math.max(...Object.values(TURN_MESSAGE_LABEL).map((value) => displayWidth(value)));
|
|
299
|
+
const labelWidth = Math.min(Math.max(1, Math.floor(budget / 2)), Math.max(preferredLabel, displayWidth(message.note ?? '')));
|
|
300
|
+
const label = fitLine(message.note ?? TURN_MESSAGE_LABEL[message.state], labelWidth);
|
|
301
|
+
const textWidth = Math.max(1, budget - labelWidth);
|
|
302
|
+
const text = padToWidth(fitLine(`"${message.text}"`, textWidth), textWidth);
|
|
303
|
+
if (message.state === 'delivered') {
|
|
304
|
+
return line(span(' ● ', { color: 'green' }), span(text, { color: 'gray', dim: true }), span(` ${label}`, { color: 'green' }));
|
|
305
|
+
}
|
|
306
|
+
if (message.state === 'sending') {
|
|
307
|
+
return line(span(` ${todoInProgressGlyph(progressTick)} `, {
|
|
308
|
+
color: TODO_IN_PROGRESS_COLOR,
|
|
309
|
+
}), span(text, { color: TODO_IN_PROGRESS_COLOR, bold: true }), span(` ${label}`, { color: TODO_IN_PROGRESS_COLOR }));
|
|
310
|
+
}
|
|
311
|
+
return line(span(' ○ ', { color: 'cyan' }), span(text, { color: 'cyan' }), span(` ${label}`, { color: 'gray' }));
|
|
312
|
+
}
|
|
313
|
+
export function buildTurnMessageLines(messages, width, progressTick = null) {
|
|
314
|
+
if (messages.length === 0)
|
|
315
|
+
return [];
|
|
316
|
+
const lines = [
|
|
317
|
+
line(span('Your messages', { color: 'cyan', bold: true })),
|
|
318
|
+
];
|
|
319
|
+
const visible = messages.slice(-(TURN_MESSAGE_PANEL_MAX_ROWS - 1));
|
|
320
|
+
const hidden = messages.length - visible.length;
|
|
321
|
+
if (hidden > 0) {
|
|
322
|
+
lines.push(line(span(` ● ${hidden} earlier`, { color: 'gray', dim: true })));
|
|
323
|
+
}
|
|
324
|
+
for (const message of visible) {
|
|
325
|
+
lines.push(turnMessageLine(message, width, progressTick));
|
|
326
|
+
}
|
|
327
|
+
return lines;
|
|
328
|
+
}
|
|
289
329
|
export function formatTodoProgress(items) {
|
|
290
330
|
const done = items.filter((item) => item.status === 'completed').length;
|
|
291
331
|
return `${done}/${items.length} done`;
|
|
@@ -341,6 +381,26 @@ export function renderTranscriptEntryLines(entry, width) {
|
|
|
341
381
|
}
|
|
342
382
|
return lines;
|
|
343
383
|
}
|
|
384
|
+
const transcriptEntryLineCache = new WeakMap();
|
|
385
|
+
const MAX_CACHED_TRANSCRIPT_WIDTHS = 2;
|
|
386
|
+
function cachedTranscriptEntryLines(entry, width) {
|
|
387
|
+
let byWidth = transcriptEntryLineCache.get(entry);
|
|
388
|
+
if (!byWidth) {
|
|
389
|
+
byWidth = new Map();
|
|
390
|
+
transcriptEntryLineCache.set(entry, byWidth);
|
|
391
|
+
}
|
|
392
|
+
const cached = byWidth.get(width);
|
|
393
|
+
if (cached)
|
|
394
|
+
return cached;
|
|
395
|
+
const lines = renderTranscriptEntryLines(entry, width);
|
|
396
|
+
if (byWidth.size >= MAX_CACHED_TRANSCRIPT_WIDTHS) {
|
|
397
|
+
const oldest = byWidth.keys().next().value;
|
|
398
|
+
if (oldest !== undefined)
|
|
399
|
+
byWidth.delete(oldest);
|
|
400
|
+
}
|
|
401
|
+
byWidth.set(width, lines);
|
|
402
|
+
return lines;
|
|
403
|
+
}
|
|
344
404
|
function renderDiffPreviewLines(preview, width, maxDiffLines = TRANSCRIPT_DIFF_PREVIEW_LINES) {
|
|
345
405
|
return [
|
|
346
406
|
plainLine(` Added ${preview.added} line${preview.added === 1 ? '' : 's'}, removed ${preview.removed} line${preview.removed === 1 ? '' : 's'}`, { color: 'gray' }),
|
|
@@ -452,7 +512,7 @@ function composerFooterLines(state) {
|
|
|
452
512
|
return lines;
|
|
453
513
|
}
|
|
454
514
|
const busyHelperText = state.queuedMessage
|
|
455
|
-
? 'Enter
|
|
515
|
+
? 'Enter sends it to the agent • ↑ edit • Esc / Ctrl+C discard'
|
|
456
516
|
: state.input
|
|
457
517
|
? 'Enter queues • Esc cancels turn • Ctrl+C clears draft'
|
|
458
518
|
: 'Enter queues • Esc / Ctrl+C cancel turn';
|
|
@@ -629,6 +689,11 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds, nowMs) {
|
|
|
629
689
|
lines.push(plainLine(''));
|
|
630
690
|
}
|
|
631
691
|
lines.push(plainLine(buildWorkingClockLine(state, elapsedSeconds), { color: 'yellow' }));
|
|
692
|
+
const turnMessages = state.turnMessages ?? [];
|
|
693
|
+
if (turnMessages.length > 0) {
|
|
694
|
+
lines.push(plainLine(''));
|
|
695
|
+
lines.push(...buildTurnMessageLines(turnMessages, width, todoProgressTick(nowMs)));
|
|
696
|
+
}
|
|
632
697
|
const todos = state.todos ?? [];
|
|
633
698
|
if (todos.length > 0) {
|
|
634
699
|
lines.push(plainLine(''));
|
|
@@ -1035,25 +1100,19 @@ function buildOverlayLines(state, width, height, nowMs) {
|
|
|
1035
1100
|
if (padded)
|
|
1036
1101
|
lines.push(plainLine(''));
|
|
1037
1102
|
}
|
|
1038
|
-
const options = [
|
|
1039
|
-
|
|
1040
|
-
{ value: 'a', label: 'Approve all remaining actions' },
|
|
1041
|
-
{ value: 'n', label: 'Deny' },
|
|
1042
|
-
];
|
|
1043
|
-
options.forEach((option, index) => {
|
|
1103
|
+
const options = ['Approve once', 'Approve all remaining actions', 'Deny'];
|
|
1104
|
+
options.forEach((label, index) => {
|
|
1044
1105
|
const selected = index === state.approvalCursor;
|
|
1045
1106
|
lines.push(line(span(selected ? '› ' : ' ', {
|
|
1046
1107
|
color: selected ? APPROVAL_ACCENT_COLOR : 'gray',
|
|
1047
|
-
}), span(
|
|
1108
|
+
}), span(label, {
|
|
1048
1109
|
color: selected ? APPROVAL_ACCENT_COLOR : undefined,
|
|
1049
1110
|
bold: selected,
|
|
1050
|
-
}), span(` ${option.label}`, {
|
|
1051
|
-
color: selected ? APPROVAL_ACCENT_COLOR : undefined,
|
|
1052
1111
|
})));
|
|
1053
1112
|
});
|
|
1054
1113
|
if (padded)
|
|
1055
1114
|
lines.push(plainLine(''));
|
|
1056
|
-
lines.push(plainLine('
|
|
1115
|
+
lines.push(plainLine('↑/↓ moves • Enter confirms • Esc denies', {
|
|
1057
1116
|
color: 'gray',
|
|
1058
1117
|
}));
|
|
1059
1118
|
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR, height);
|
|
@@ -1154,14 +1213,18 @@ function sliceTranscriptLines(lines, maxLines, scrollOffset) {
|
|
|
1154
1213
|
export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, nowMs = 0) {
|
|
1155
1214
|
const contentWidth = Math.max(20, Math.floor(cols * 0.95) - 2);
|
|
1156
1215
|
const gutter = Math.max(Math.floor((cols - contentWidth) / 2), 0);
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1216
|
+
const buildTranscriptLines = (wrapWidth) => {
|
|
1217
|
+
const blocks = state.transcript.map((entry) => cachedTranscriptEntryLines(entry, wrapWidth));
|
|
1218
|
+
const lines = [];
|
|
1219
|
+
blocks.forEach((block, index) => {
|
|
1220
|
+
if (index > 0) {
|
|
1221
|
+
lines.push(plainLine(''));
|
|
1222
|
+
}
|
|
1223
|
+
lines.push(...block);
|
|
1224
|
+
});
|
|
1225
|
+
return lines;
|
|
1226
|
+
};
|
|
1227
|
+
let transcriptLines = buildTranscriptLines(contentWidth);
|
|
1165
1228
|
const sections = [];
|
|
1166
1229
|
const liveLines = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds, nowMs);
|
|
1167
1230
|
if (liveLines.length > 0) {
|
|
@@ -1174,11 +1237,10 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
|
|
|
1174
1237
|
composerLines.push(line(span('Starting a new conversation…', { color: 'gray', dim: true })));
|
|
1175
1238
|
}
|
|
1176
1239
|
else if (state.queuedMessage) {
|
|
1177
|
-
const preview = truncate(state.queuedMessage.body.trim().replace(/\s+/g, ' '), 60);
|
|
1178
1240
|
const imageCount = state.queuedMessage.imageAttachments.length;
|
|
1179
|
-
composerLines.push(line(span(
|
|
1180
|
-
?
|
|
1181
|
-
:
|
|
1241
|
+
composerLines.push(line(span('Queued', { color: 'cyan', bold: true }), span(' Enter', { color: 'cyan', bold: true }), span(imageCount > 0
|
|
1242
|
+
? ' sends it with the next prompt'
|
|
1243
|
+
: ' sends it to the agent now', { color: 'gray' }), span(' · ↑', { color: 'cyan', bold: true }), span(' edit', { color: 'gray' }), span(' · Esc', { color: 'cyan', bold: true }), span(' discard', { color: 'gray' })));
|
|
1182
1244
|
}
|
|
1183
1245
|
else {
|
|
1184
1246
|
const promptLabel = state.busy ? 'queue> ' : '❯ ';
|
|
@@ -1213,7 +1275,11 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
|
|
|
1213
1275
|
? 0
|
|
1214
1276
|
: 4;
|
|
1215
1277
|
const transcriptBudget = Math.max(1, rows - reservedLines - composerReserve - 1);
|
|
1216
|
-
|
|
1278
|
+
let transcriptScrollLimit = Math.max(0, transcriptLines.length - transcriptBudget);
|
|
1279
|
+
if (transcriptScrollLimit > 0 && contentWidth > 2) {
|
|
1280
|
+
transcriptLines = buildTranscriptLines(contentWidth - 2);
|
|
1281
|
+
transcriptScrollLimit = Math.max(0, transcriptLines.length - transcriptBudget);
|
|
1282
|
+
}
|
|
1217
1283
|
const transcriptScrollOffset = Math.min(Math.max(state.transcriptScrollOffset, 0), transcriptScrollLimit);
|
|
1218
1284
|
sections.unshift({
|
|
1219
1285
|
kind: 'transcript',
|
|
@@ -275,9 +275,9 @@ function wrapInlineToLines(text, width, bodyColor, prefix = '') {
|
|
|
275
275
|
};
|
|
276
276
|
const appendSpan = (part) => {
|
|
277
277
|
let current = rows[rows.length - 1];
|
|
278
|
-
const limit = safeWidth - (rows.length === 1 && indent ? displayWidth(prefix) : 0);
|
|
279
278
|
let remaining = part.text;
|
|
280
279
|
while (remaining.length > 0) {
|
|
280
|
+
const limit = safeWidth - (rows.length === 1 && indent ? displayWidth(prefix) : 0);
|
|
281
281
|
const room = limit - rowWidth;
|
|
282
282
|
if (room <= 0) {
|
|
283
283
|
startRow();
|
|
@@ -291,8 +291,17 @@ function wrapInlineToLines(text, width, bodyColor, prefix = '') {
|
|
|
291
291
|
remaining = '';
|
|
292
292
|
break;
|
|
293
293
|
}
|
|
294
|
+
if (rowWidth > 0) {
|
|
295
|
+
if (/^\s+$/.test(remaining)) {
|
|
296
|
+
remaining = '';
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
startRow();
|
|
300
|
+
current = rows[rows.length - 1];
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
294
303
|
const head = sliceToWidth(remaining, room);
|
|
295
|
-
if (displayWidth(head) > room && current.length > 0) {
|
|
304
|
+
if (!head || (displayWidth(head) > room && current.length > 0)) {
|
|
296
305
|
startRow();
|
|
297
306
|
current = rows[rows.length - 1];
|
|
298
307
|
continue;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readClipboardImage, readClipboardText } from '../../core/clipboard.js';
|
|
2
|
-
import { applySlashCommandSuggestion, buildModelPickerOptions, deleteAtCursor, deleteBeforeCursor, getApprovalChoiceForCursor, getInputCommandToken, getNextApprovalCursor, getNextModelPickerIndex, getSlashCommandSuggestions, insertAtCursor, isExactSlashCommandToken, navigatePromptHistory,
|
|
2
|
+
import { applySlashCommandSuggestion, buildModelPickerOptions, deleteAtCursor, deleteBeforeCursor, getApprovalChoiceForCursor, getInputCommandToken, getNextApprovalCursor, getNextModelPickerIndex, getSlashCommandSuggestions, insertAtCursor, isExactSlashCommandToken, navigatePromptHistory, shouldRemountLiveFrameForComposerInputChange, } from '../repl.js';
|
|
3
3
|
import { buildPastePlaceholder, shouldCollapsePaste, } from '../paste-collapse.js';
|
|
4
4
|
import { handleUserInputPromptEvent, } from './user-input.js';
|
|
5
5
|
const APPROVAL_PREVIEW_PAGE_ROWS = 3;
|
|
@@ -11,6 +11,13 @@ function isClipboardImagePasteKey(key) {
|
|
|
11
11
|
}
|
|
12
12
|
return key.ctrl && key.input === 'v' && !key.shift && !key.meta;
|
|
13
13
|
}
|
|
14
|
+
export const APPROVAL_INPUT_GUARD_MS = 500;
|
|
15
|
+
function approvalIsGuarded(state) {
|
|
16
|
+
const openedAt = state.approvalOpenedAt;
|
|
17
|
+
if (typeof openedAt !== 'number')
|
|
18
|
+
return false;
|
|
19
|
+
return Date.now() - openedAt < APPROVAL_INPUT_GUARD_MS;
|
|
20
|
+
}
|
|
14
21
|
function applyUserInputPromptEvent(store, handlers, event) {
|
|
15
22
|
const current = store.getState();
|
|
16
23
|
if (!current.userInputPrompt)
|
|
@@ -114,6 +121,16 @@ function scrollTranscript(store, handlers, delta) {
|
|
|
114
121
|
};
|
|
115
122
|
});
|
|
116
123
|
}
|
|
124
|
+
function scrollTranscriptTo(store, handlers, offset) {
|
|
125
|
+
store.update((current) => {
|
|
126
|
+
const limit = handlers.getTranscriptScrollLimit?.() ??
|
|
127
|
+
current.transcript.reduce((total, entry) => total + 2 + (entry.body ? entry.body.split('\n').length : 0), 0);
|
|
128
|
+
return {
|
|
129
|
+
...current,
|
|
130
|
+
transcriptScrollOffset: Math.max(0, Math.min(Math.trunc(offset), limit)),
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
117
134
|
function scrollApprovalPreview(store, handlers, delta) {
|
|
118
135
|
if (delta === 0)
|
|
119
136
|
return;
|
|
@@ -182,6 +199,10 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
182
199
|
scrollTranscript(store, handlers, Math.trunc(event.deltaLines));
|
|
183
200
|
return;
|
|
184
201
|
}
|
|
202
|
+
if (event.kind === 'transcriptScrollTo') {
|
|
203
|
+
scrollTranscriptTo(store, handlers, Number(event.offset ?? 0));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
185
206
|
if (event.kind !== 'key')
|
|
186
207
|
return;
|
|
187
208
|
const key = event;
|
|
@@ -269,15 +290,6 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
269
290
|
}
|
|
270
291
|
return;
|
|
271
292
|
}
|
|
272
|
-
const directChoice = key.ctrl || key.meta ? null : resolveApprovalChoiceFromInput(key.input);
|
|
273
|
-
if (directChoice) {
|
|
274
|
-
void handlers.onResolveApproval(directChoice);
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
if (key.escape) {
|
|
278
|
-
void handlers.onResolveApproval('n');
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
293
|
if (key.upArrow || key.downArrow) {
|
|
282
294
|
store.update((current) => ({
|
|
283
295
|
...current,
|
|
@@ -285,6 +297,13 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
285
297
|
}));
|
|
286
298
|
return;
|
|
287
299
|
}
|
|
300
|
+
if (approvalIsGuarded(state)) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (key.escape) {
|
|
304
|
+
void handlers.onResolveApproval('n');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
288
307
|
if (key.returnKey) {
|
|
289
308
|
void handlers.onResolveApproval(getApprovalChoiceForCursor(state.approvalCursor));
|
|
290
309
|
}
|
|
@@ -422,6 +441,15 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
422
441
|
handlers.onCycleAgentMode();
|
|
423
442
|
return;
|
|
424
443
|
}
|
|
444
|
+
if (state.busy && state.queuedMessage) {
|
|
445
|
+
if (key.returnKey) {
|
|
446
|
+
void handlers.onFireQueuedMessage?.();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (!key.upArrow) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
425
453
|
if (commandPaletteActive && (key.upArrow || key.downArrow)) {
|
|
426
454
|
store.update((current) => ({
|
|
427
455
|
...current,
|
|
@@ -430,7 +458,7 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
430
458
|
return;
|
|
431
459
|
}
|
|
432
460
|
if (key.upArrow) {
|
|
433
|
-
if (state.busy && state.
|
|
461
|
+
if (state.busy && state.queuedMessage) {
|
|
434
462
|
handlers.onLiveFrameShapeChange();
|
|
435
463
|
store.update((current) => {
|
|
436
464
|
const queued = current.queuedMessage;
|
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.16",
|
|
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.16",
|
|
41
|
+
"@thegitai/tui-darwin-x64": "1.0.0-preview.16",
|
|
42
|
+
"@thegitai/tui-linux-x64": "1.0.0-preview.16",
|
|
43
|
+
"@thegitai/tui-win32-x64": "1.0.0-preview.16",
|
|
44
44
|
"@vscode/ripgrep": "1.18.0"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|