@thegitai/cli 1.0.0-preview.15 → 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/build-frame.js +54 -16
- package/dist/src/ui/tui/shell-input.js +25 -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();
|
|
@@ -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`;
|
|
@@ -472,7 +512,7 @@ function composerFooterLines(state) {
|
|
|
472
512
|
return lines;
|
|
473
513
|
}
|
|
474
514
|
const busyHelperText = state.queuedMessage
|
|
475
|
-
? 'Enter
|
|
515
|
+
? 'Enter sends it to the agent • ↑ edit • Esc / Ctrl+C discard'
|
|
476
516
|
: state.input
|
|
477
517
|
? 'Enter queues • Esc cancels turn • Ctrl+C clears draft'
|
|
478
518
|
: 'Enter queues • Esc / Ctrl+C cancel turn';
|
|
@@ -649,6 +689,11 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds, nowMs) {
|
|
|
649
689
|
lines.push(plainLine(''));
|
|
650
690
|
}
|
|
651
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
|
+
}
|
|
652
697
|
const todos = state.todos ?? [];
|
|
653
698
|
if (todos.length > 0) {
|
|
654
699
|
lines.push(plainLine(''));
|
|
@@ -1055,25 +1100,19 @@ function buildOverlayLines(state, width, height, nowMs) {
|
|
|
1055
1100
|
if (padded)
|
|
1056
1101
|
lines.push(plainLine(''));
|
|
1057
1102
|
}
|
|
1058
|
-
const options = [
|
|
1059
|
-
|
|
1060
|
-
{ value: 'a', label: 'Approve all remaining actions' },
|
|
1061
|
-
{ value: 'n', label: 'Deny' },
|
|
1062
|
-
];
|
|
1063
|
-
options.forEach((option, index) => {
|
|
1103
|
+
const options = ['Approve once', 'Approve all remaining actions', 'Deny'];
|
|
1104
|
+
options.forEach((label, index) => {
|
|
1064
1105
|
const selected = index === state.approvalCursor;
|
|
1065
1106
|
lines.push(line(span(selected ? '› ' : ' ', {
|
|
1066
1107
|
color: selected ? APPROVAL_ACCENT_COLOR : 'gray',
|
|
1067
|
-
}), span(
|
|
1108
|
+
}), span(label, {
|
|
1068
1109
|
color: selected ? APPROVAL_ACCENT_COLOR : undefined,
|
|
1069
1110
|
bold: selected,
|
|
1070
|
-
}), span(` ${option.label}`, {
|
|
1071
|
-
color: selected ? APPROVAL_ACCENT_COLOR : undefined,
|
|
1072
1111
|
})));
|
|
1073
1112
|
});
|
|
1074
1113
|
if (padded)
|
|
1075
1114
|
lines.push(plainLine(''));
|
|
1076
|
-
lines.push(plainLine('
|
|
1115
|
+
lines.push(plainLine('↑/↓ moves • Enter confirms • Esc denies', {
|
|
1077
1116
|
color: 'gray',
|
|
1078
1117
|
}));
|
|
1079
1118
|
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR, height);
|
|
@@ -1198,11 +1237,10 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
|
|
|
1198
1237
|
composerLines.push(line(span('Starting a new conversation…', { color: 'gray', dim: true })));
|
|
1199
1238
|
}
|
|
1200
1239
|
else if (state.queuedMessage) {
|
|
1201
|
-
const preview = truncate(state.queuedMessage.body.trim().replace(/\s+/g, ' '), 60);
|
|
1202
1240
|
const imageCount = state.queuedMessage.imageAttachments.length;
|
|
1203
|
-
composerLines.push(line(span(
|
|
1204
|
-
?
|
|
1205
|
-
:
|
|
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' })));
|
|
1206
1244
|
}
|
|
1207
1245
|
else {
|
|
1208
1246
|
const promptLabel = state.busy ? 'queue> ' : '❯ ';
|
|
@@ -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)
|
|
@@ -283,15 +290,6 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
283
290
|
}
|
|
284
291
|
return;
|
|
285
292
|
}
|
|
286
|
-
const directChoice = key.ctrl || key.meta ? null : resolveApprovalChoiceFromInput(key.input);
|
|
287
|
-
if (directChoice) {
|
|
288
|
-
void handlers.onResolveApproval(directChoice);
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
if (key.escape) {
|
|
292
|
-
void handlers.onResolveApproval('n');
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
293
|
if (key.upArrow || key.downArrow) {
|
|
296
294
|
store.update((current) => ({
|
|
297
295
|
...current,
|
|
@@ -299,6 +297,13 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
299
297
|
}));
|
|
300
298
|
return;
|
|
301
299
|
}
|
|
300
|
+
if (approvalIsGuarded(state)) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (key.escape) {
|
|
304
|
+
void handlers.onResolveApproval('n');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
302
307
|
if (key.returnKey) {
|
|
303
308
|
void handlers.onResolveApproval(getApprovalChoiceForCursor(state.approvalCursor));
|
|
304
309
|
}
|
|
@@ -436,6 +441,15 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
436
441
|
handlers.onCycleAgentMode();
|
|
437
442
|
return;
|
|
438
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
|
+
}
|
|
439
453
|
if (commandPaletteActive && (key.upArrow || key.downArrow)) {
|
|
440
454
|
store.update((current) => ({
|
|
441
455
|
...current,
|
|
@@ -444,7 +458,7 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
444
458
|
return;
|
|
445
459
|
}
|
|
446
460
|
if (key.upArrow) {
|
|
447
|
-
if (state.busy && state.
|
|
461
|
+
if (state.busy && state.queuedMessage) {
|
|
448
462
|
handlers.onLiveFrameShapeChange();
|
|
449
463
|
store.update((current) => {
|
|
450
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": {
|