@thegitai/cli 1.0.0-preview.11 → 1.0.0-preview.13
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 +1 -1
- package/dist/src/executor.js +1 -1
- package/dist/src/ui/repl.js +104 -17
- package/dist/src/ui/tui/build-frame.js +221 -49
- package/dist/src/ui/tui/shell-input.js +39 -22
- package/package.json +5 -5
package/dist/src/api/chat.js
CHANGED
|
@@ -160,7 +160,7 @@ function publicStatusMessage(data) {
|
|
|
160
160
|
? event.toolName
|
|
161
161
|
: 'tool';
|
|
162
162
|
if (event.phase === 'thinking')
|
|
163
|
-
return '
|
|
163
|
+
return 'Exploring options...';
|
|
164
164
|
if (event.phase === 'analyzing_image') {
|
|
165
165
|
return (event.imageCount ?? 1) > 1 ? 'Analyzing images...' : 'Analyzing image...';
|
|
166
166
|
}
|
package/dist/src/executor.js
CHANGED
|
@@ -596,7 +596,7 @@ export function commandUsesSudo(command) {
|
|
|
596
596
|
}
|
|
597
597
|
export function sudoPromptFromTail(text) {
|
|
598
598
|
const tail = text.slice(-1000).replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
|
|
599
|
-
const match = tail.match(/(?:\[sudo\][^\r\n]*password[^\r\n]*:
|
|
599
|
+
const match = tail.match(/(?:\[sudo\][^\r\n]*password[^\r\n]*: ?|\[?sudo[^\r\n]*password[^\r\n]*: ?|password[^\r\n]*: ?)$/i);
|
|
600
600
|
return match?.[0] ?? null;
|
|
601
601
|
}
|
|
602
602
|
function isSudoPromptLine(text) {
|
package/dist/src/ui/repl.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRatatuiBridge } from './tui/bridge.js';
|
|
2
|
-
import { buildTuiFrame, formatJobElapsed, formatTodoProgress, renderTranscriptEntryLines, } from './tui/build-frame.js';
|
|
2
|
+
import { approvalScrollLimit, buildTuiFrame, formatJobElapsed, formatTodoProgress, pickThinkingFallbackPhrase, renderTranscriptEntryLines, THINKING_FALLBACK_PHRASES, } from './tui/build-frame.js';
|
|
3
3
|
import { createTerminalTitleController } from './tui/terminal-title.js';
|
|
4
4
|
import { captureTerminalWrites, releaseTerminalWrites, } from './tui/terminal-writes.js';
|
|
5
5
|
export { getSlashCommandSuggestions } from './tui/build-frame.js';
|
|
@@ -966,9 +966,11 @@ function createInitialShellState(session, serverModels, debugUi) {
|
|
|
966
966
|
analyzingImages: 0,
|
|
967
967
|
approvalCursor: getDefaultApprovalCursor(),
|
|
968
968
|
approvalPrompt: null,
|
|
969
|
+
approvalScrollOffset: 0,
|
|
969
970
|
autoYes: session.autoYes,
|
|
970
971
|
backgroundJobs: [],
|
|
971
972
|
busy: false,
|
|
973
|
+
busyPausedAt: null,
|
|
972
974
|
busySince: null,
|
|
973
975
|
clockNow: Date.now(),
|
|
974
976
|
commandCursor: 0,
|
|
@@ -1074,10 +1076,21 @@ function splitThinkingLines(text) {
|
|
|
1074
1076
|
.filter(Boolean));
|
|
1075
1077
|
}
|
|
1076
1078
|
const LIVE_STATUS_PANEL_MAX_CHARS = 72;
|
|
1079
|
+
const TOOL_PROGRESS_STATUS_PATTERN = /^(?:Tool:\s|Running\s\S+(?:\slocally)?\.\.\.$)/i;
|
|
1080
|
+
export function isToolProgressStatus(status) {
|
|
1081
|
+
return TOOL_PROGRESS_STATUS_PATTERN.test(String(status ?? '').trim());
|
|
1082
|
+
}
|
|
1083
|
+
function holdOrPickFallback(currentTitle) {
|
|
1084
|
+
return THINKING_FALLBACK_PHRASES.includes(currentTitle.trim())
|
|
1085
|
+
? currentTitle
|
|
1086
|
+
: pickThinkingFallbackPhrase();
|
|
1087
|
+
}
|
|
1077
1088
|
function liveStatusPanelTitle(status) {
|
|
1078
1089
|
const text = String(status ?? '').trim();
|
|
1079
1090
|
if (!text || text.includes('\n'))
|
|
1080
1091
|
return '';
|
|
1092
|
+
if (isToolProgressStatus(text))
|
|
1093
|
+
return '';
|
|
1081
1094
|
return text.length <= LIVE_STATUS_PANEL_MAX_CHARS ? text : '';
|
|
1082
1095
|
}
|
|
1083
1096
|
function thinkingPanelFromStatus(status) {
|
|
@@ -1103,7 +1116,7 @@ function thinkingPanelFromStatus(status) {
|
|
|
1103
1116
|
const bodyLines = rawLines.length > 1 && rawLines[0].length <= 72 ? rawLines.slice(1) : rawLines;
|
|
1104
1117
|
return {
|
|
1105
1118
|
title,
|
|
1106
|
-
notes: splitThinkingLines(bodyLines.join('\n')).slice(
|
|
1119
|
+
notes: splitThinkingLines(bodyLines.join('\n')).slice(0, 3),
|
|
1107
1120
|
};
|
|
1108
1121
|
}
|
|
1109
1122
|
export const EXIT_CTRL_C_CONFIRM_MESSAGE = 'Press Ctrl+C again to quit.';
|
|
@@ -1254,6 +1267,30 @@ export function resolveApprovalChoiceFromInput(input) {
|
|
|
1254
1267
|
return 'n';
|
|
1255
1268
|
return null;
|
|
1256
1269
|
}
|
|
1270
|
+
export function pauseBusyClock(state, nowMs) {
|
|
1271
|
+
if (state.busySince === null || state.busyPausedAt !== null)
|
|
1272
|
+
return state;
|
|
1273
|
+
return { ...state, busyPausedAt: nowMs };
|
|
1274
|
+
}
|
|
1275
|
+
export function resumeBusyClock(state, nowMs) {
|
|
1276
|
+
if (state.busyPausedAt === null)
|
|
1277
|
+
return state;
|
|
1278
|
+
return {
|
|
1279
|
+
...state,
|
|
1280
|
+
busyPausedAt: null,
|
|
1281
|
+
busySince: state.busySince === null
|
|
1282
|
+
? null
|
|
1283
|
+
: state.busySince + Math.max(0, nowMs - state.busyPausedAt),
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
export function busyElapsedMs(state, nowMs) {
|
|
1287
|
+
if (state.busySince === null)
|
|
1288
|
+
return null;
|
|
1289
|
+
return Math.max(0, (state.busyPausedAt ?? nowMs) - state.busySince);
|
|
1290
|
+
}
|
|
1291
|
+
export function busyElapsedSeconds(state, nowMs) {
|
|
1292
|
+
return Math.floor((busyElapsedMs(state, nowMs) ?? 0) / 1000);
|
|
1293
|
+
}
|
|
1257
1294
|
export function buildModelPickerOptions(currentModelId, serverModels) {
|
|
1258
1295
|
return serverModels.map((model) => ({
|
|
1259
1296
|
id: model.id,
|
|
@@ -1339,6 +1376,36 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1339
1376
|
let exitCtrlCArmed = false;
|
|
1340
1377
|
let exitCtrlCTimer = null;
|
|
1341
1378
|
let transientStatusTimer = null;
|
|
1379
|
+
const THINKING_MIN_DWELL_MS = 3_000;
|
|
1380
|
+
let thinkingAppliedAt = 0;
|
|
1381
|
+
let pendingThinking = null;
|
|
1382
|
+
const applyThinking = (next) => {
|
|
1383
|
+
thinkingAppliedAt = Date.now();
|
|
1384
|
+
pendingThinking = null;
|
|
1385
|
+
store.update((current) => ({
|
|
1386
|
+
...current,
|
|
1387
|
+
thinkingTitle: next.title || holdOrPickFallback(current.thinkingTitle),
|
|
1388
|
+
thinkingNotes: next.notes,
|
|
1389
|
+
}));
|
|
1390
|
+
};
|
|
1391
|
+
const queueThinkingUpdate = (next) => {
|
|
1392
|
+
if (Date.now() - thinkingAppliedAt >= THINKING_MIN_DWELL_MS) {
|
|
1393
|
+
applyThinking(next);
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
pendingThinking = next;
|
|
1397
|
+
};
|
|
1398
|
+
const flushPendingThinking = () => {
|
|
1399
|
+
if (!pendingThinking)
|
|
1400
|
+
return;
|
|
1401
|
+
if (Date.now() - thinkingAppliedAt < THINKING_MIN_DWELL_MS)
|
|
1402
|
+
return;
|
|
1403
|
+
applyThinking(pendingThinking);
|
|
1404
|
+
};
|
|
1405
|
+
const resetThinkingPacer = () => {
|
|
1406
|
+
thinkingAppliedAt = 0;
|
|
1407
|
+
pendingThinking = null;
|
|
1408
|
+
};
|
|
1342
1409
|
const done = new Promise((resolve) => {
|
|
1343
1410
|
resolveDone = resolve;
|
|
1344
1411
|
});
|
|
@@ -1378,9 +1445,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1378
1445
|
return;
|
|
1379
1446
|
const state = store.getState();
|
|
1380
1447
|
syncTerminalTitle();
|
|
1381
|
-
const elapsedSeconds = state.
|
|
1382
|
-
? Math.max(0, Math.floor((Date.now() - state.busySince) / 1000))
|
|
1383
|
-
: 0;
|
|
1448
|
+
const elapsedSeconds = busyElapsedSeconds(state, Date.now());
|
|
1384
1449
|
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds, Date.now()));
|
|
1385
1450
|
};
|
|
1386
1451
|
const remountTui = async () => {
|
|
@@ -1512,9 +1577,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1512
1577
|
resolveApprovalChoice = null;
|
|
1513
1578
|
pendingResolve('n');
|
|
1514
1579
|
store.update((current) => ({
|
|
1515
|
-
...current,
|
|
1580
|
+
...resumeBusyClock(current, Date.now()),
|
|
1516
1581
|
approvalCursor: getDefaultApprovalCursor(),
|
|
1517
1582
|
approvalPrompt: null,
|
|
1583
|
+
approvalScrollOffset: 0,
|
|
1518
1584
|
}));
|
|
1519
1585
|
};
|
|
1520
1586
|
const dismissPendingSudoPassword = () => {
|
|
@@ -1527,7 +1593,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1527
1593
|
sudoPasswordBuffer = '';
|
|
1528
1594
|
pendingResolve(null);
|
|
1529
1595
|
store.update((current) => ({
|
|
1530
|
-
...current,
|
|
1596
|
+
...resumeBusyClock(current, Date.now()),
|
|
1531
1597
|
sudoPrompt: null,
|
|
1532
1598
|
}));
|
|
1533
1599
|
};
|
|
@@ -1538,6 +1604,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1538
1604
|
dismissPendingApproval();
|
|
1539
1605
|
dismissPendingSudoPassword();
|
|
1540
1606
|
activeTurnGeneration += 1;
|
|
1607
|
+
resetThinkingPacer();
|
|
1541
1608
|
activeTurnAbort?.abort();
|
|
1542
1609
|
activeTurnAbort = null;
|
|
1543
1610
|
cancelActiveCommand();
|
|
@@ -1555,6 +1622,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1555
1622
|
activeTurnInput: '',
|
|
1556
1623
|
activeTurnInputPreformatted: false,
|
|
1557
1624
|
busy: false,
|
|
1625
|
+
busyPausedAt: null,
|
|
1558
1626
|
busySince: null,
|
|
1559
1627
|
commandLog: [],
|
|
1560
1628
|
cursor: queued ? queued.body.length : current.cursor,
|
|
@@ -1568,7 +1636,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1568
1636
|
thinkingTitle: '',
|
|
1569
1637
|
thinkingNotes: [],
|
|
1570
1638
|
workingTools: [],
|
|
1571
|
-
tokenUsage: formatClientTokenUsage(current
|
|
1639
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
1572
1640
|
}));
|
|
1573
1641
|
appendStaticEntries(cancelledEntries);
|
|
1574
1642
|
lastTurnStartedAt = null;
|
|
@@ -1719,7 +1787,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1719
1787
|
cleanupSudoPasswordPrompt = null;
|
|
1720
1788
|
}
|
|
1721
1789
|
store.update((current) => ({
|
|
1722
|
-
...current,
|
|
1790
|
+
...pauseBusyClock(current, Date.now()),
|
|
1723
1791
|
sudoPrompt: {
|
|
1724
1792
|
command,
|
|
1725
1793
|
passwordLength: 0,
|
|
@@ -1739,7 +1807,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1739
1807
|
cleanupSudoPasswordPrompt = null;
|
|
1740
1808
|
sudoPasswordBuffer = '';
|
|
1741
1809
|
store.update((current) => ({
|
|
1742
|
-
...current,
|
|
1810
|
+
...resumeBusyClock(current, Date.now()),
|
|
1743
1811
|
sudoPrompt: null,
|
|
1744
1812
|
status: current.sudoPrompt?.returnStatus ?? current.status,
|
|
1745
1813
|
}));
|
|
@@ -1753,7 +1821,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1753
1821
|
cleanupSudoPasswordPrompt = null;
|
|
1754
1822
|
sudoPasswordBuffer = '';
|
|
1755
1823
|
store.update((current) => ({
|
|
1756
|
-
...current,
|
|
1824
|
+
...resumeBusyClock(current, Date.now()),
|
|
1757
1825
|
sudoPrompt: null,
|
|
1758
1826
|
status: current.sudoPrompt?.returnStatus ?? current.status,
|
|
1759
1827
|
}));
|
|
@@ -1780,8 +1848,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1780
1848
|
}
|
|
1781
1849
|
resolveApprovalChoice = resolve;
|
|
1782
1850
|
store.update((current) => ({
|
|
1783
|
-
...current,
|
|
1851
|
+
...pauseBusyClock(current, Date.now()),
|
|
1784
1852
|
approvalCursor: getDefaultApprovalCursor(),
|
|
1853
|
+
approvalScrollOffset: 0,
|
|
1785
1854
|
approvalPrompt: {
|
|
1786
1855
|
title,
|
|
1787
1856
|
body,
|
|
@@ -1799,9 +1868,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1799
1868
|
const pendingResolve = resolveApprovalChoice;
|
|
1800
1869
|
resolveApprovalChoice = null;
|
|
1801
1870
|
store.update((next) => ({
|
|
1802
|
-
...next,
|
|
1871
|
+
...resumeBusyClock(next, Date.now()),
|
|
1803
1872
|
approvalCursor: getDefaultApprovalCursor(),
|
|
1804
1873
|
approvalPrompt: null,
|
|
1874
|
+
approvalScrollOffset: 0,
|
|
1805
1875
|
status: current.approvalPrompt?.returnStatus ?? next.status,
|
|
1806
1876
|
}));
|
|
1807
1877
|
pendingResolve?.(choice);
|
|
@@ -2009,6 +2079,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2009
2079
|
store.update((next) => ({
|
|
2010
2080
|
...next,
|
|
2011
2081
|
busy: true,
|
|
2082
|
+
busyPausedAt: null,
|
|
2012
2083
|
busySince: resumeStartedAt,
|
|
2013
2084
|
clockNow: resumeStartedAt,
|
|
2014
2085
|
status: 'Loading session...',
|
|
@@ -2025,6 +2096,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2025
2096
|
store.update((next) => ({
|
|
2026
2097
|
...next,
|
|
2027
2098
|
busy: false,
|
|
2099
|
+
busyPausedAt: null,
|
|
2028
2100
|
busySince: null,
|
|
2029
2101
|
resumePickerFilter: '',
|
|
2030
2102
|
resumePickerIndex: 0,
|
|
@@ -2048,6 +2120,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2048
2120
|
store.update((next) => ({
|
|
2049
2121
|
...next,
|
|
2050
2122
|
busy: false,
|
|
2123
|
+
busyPausedAt: null,
|
|
2051
2124
|
busySince: null,
|
|
2052
2125
|
resumePickerFilter: '',
|
|
2053
2126
|
resumePickerIndex: 0,
|
|
@@ -2273,6 +2346,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2273
2346
|
store.update((current) => ({
|
|
2274
2347
|
...current,
|
|
2275
2348
|
busy: true,
|
|
2349
|
+
busyPausedAt: null,
|
|
2276
2350
|
busySince: Date.now(),
|
|
2277
2351
|
imageAttachments: [],
|
|
2278
2352
|
queuedMessage: null,
|
|
@@ -2283,6 +2357,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2283
2357
|
store.update((current) => ({
|
|
2284
2358
|
...current,
|
|
2285
2359
|
busy: false,
|
|
2360
|
+
busyPausedAt: null,
|
|
2286
2361
|
busySince: null,
|
|
2287
2362
|
status: 'Ready',
|
|
2288
2363
|
}));
|
|
@@ -2310,6 +2385,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2310
2385
|
store.update((current) => ({
|
|
2311
2386
|
...current,
|
|
2312
2387
|
busy: false,
|
|
2388
|
+
busyPausedAt: null,
|
|
2313
2389
|
busySince: null,
|
|
2314
2390
|
status: 'Ready',
|
|
2315
2391
|
}));
|
|
@@ -2335,12 +2411,14 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2335
2411
|
};
|
|
2336
2412
|
pendingTurnEntries = [];
|
|
2337
2413
|
queueTurnEntry(userEntry);
|
|
2414
|
+
resetThinkingPacer();
|
|
2338
2415
|
store.update((current) => ({
|
|
2339
2416
|
...current,
|
|
2340
2417
|
activeTurnInput: input,
|
|
2341
2418
|
activeTurnInputPreformatted: preformatted,
|
|
2342
2419
|
analyzingImages: 0,
|
|
2343
2420
|
busy: true,
|
|
2421
|
+
busyPausedAt: null,
|
|
2344
2422
|
busySince: turnStartedAt,
|
|
2345
2423
|
clockNow: turnStartedAt,
|
|
2346
2424
|
status: 'Running turn...',
|
|
@@ -2402,6 +2480,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2402
2480
|
store.update((current) => ({
|
|
2403
2481
|
...current,
|
|
2404
2482
|
busy: false,
|
|
2483
|
+
busyPausedAt: null,
|
|
2405
2484
|
busySince: null,
|
|
2406
2485
|
status: result.waitingForApproval ? 'Awaiting approval' : 'Ready',
|
|
2407
2486
|
tokenUsage: formatClientTokenUsage(Date.now() - turnStartedAt, latestUsageSummary),
|
|
@@ -2423,6 +2502,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2423
2502
|
activeTurnInput: '',
|
|
2424
2503
|
activeTurnInputPreformatted: false,
|
|
2425
2504
|
busy: false,
|
|
2505
|
+
busyPausedAt: null,
|
|
2426
2506
|
busySince: null,
|
|
2427
2507
|
commandLog: [],
|
|
2428
2508
|
imageAttachments: [],
|
|
@@ -2474,16 +2554,18 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2474
2554
|
store.update((current) => ({
|
|
2475
2555
|
...current,
|
|
2476
2556
|
status: message,
|
|
2477
|
-
|
|
2478
|
-
thinkingNotes: panel ? panel.notes : [],
|
|
2479
|
-
tokenUsage: formatClientTokenUsage(current.busySince == null ? null : Date.now() - current.busySince, latestUsageSummary),
|
|
2557
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
2480
2558
|
}));
|
|
2559
|
+
queueThinkingUpdate({
|
|
2560
|
+
title: panel ? panel.title : liveStatusPanelTitle(message),
|
|
2561
|
+
notes: panel ? panel.notes : [],
|
|
2562
|
+
});
|
|
2481
2563
|
};
|
|
2482
2564
|
session.onContextLog = (message) => {
|
|
2483
2565
|
store.update((current) => ({
|
|
2484
2566
|
...current,
|
|
2485
2567
|
contextStatus: message,
|
|
2486
|
-
tokenUsage: formatClientTokenUsage(current
|
|
2568
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
2487
2569
|
}));
|
|
2488
2570
|
};
|
|
2489
2571
|
session.onToolEvent = (event) => {
|
|
@@ -2592,6 +2674,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2592
2674
|
return choice === 'y';
|
|
2593
2675
|
};
|
|
2594
2676
|
const shellInputHandlers = {
|
|
2677
|
+
getApprovalScrollLimit: () => {
|
|
2678
|
+
const contentWidth = Math.max(20, Math.floor(terminalCols * 0.95) - 2);
|
|
2679
|
+
return approvalScrollLimit(store.getState().approvalPrompt, contentWidth, terminalRows);
|
|
2680
|
+
},
|
|
2595
2681
|
getTranscriptScrollLimit: () => {
|
|
2596
2682
|
const contentWidth = Math.max(20, Math.floor(terminalCols * 0.95) - 2);
|
|
2597
2683
|
const blocks = store.getState().transcript.map((entry) => renderTranscriptEntryLines(entry, contentWidth));
|
|
@@ -2619,6 +2705,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2619
2705
|
const spinnerTimer = setInterval(() => {
|
|
2620
2706
|
spinnerFrame = (spinnerFrame + 1) % 6;
|
|
2621
2707
|
if (store.getState().busy) {
|
|
2708
|
+
flushPendingThinking();
|
|
2622
2709
|
renderCurrentFrame();
|
|
2623
2710
|
}
|
|
2624
2711
|
}, 120);
|
|
@@ -10,11 +10,25 @@ const TODO_IN_PROGRESS_COLOR = 'ansi256(214)';
|
|
|
10
10
|
const COMMAND_PREVIEW_LINES = 10;
|
|
11
11
|
const WORKING_TOOL_PREVIEW_ROWS = 3;
|
|
12
12
|
const TRANSCRIPT_DIFF_PREVIEW_LINES = 24;
|
|
13
|
+
const APPROVAL_PREVIEW_MAX_ROWS = 20;
|
|
14
|
+
const APPROVAL_PREVIEW_MIN_ROWS = 4;
|
|
15
|
+
const APPROVAL_OVERLAY_CHROME_ROWS = 11;
|
|
16
|
+
const APPROVAL_PADDING_CHROME_ROWS = 4;
|
|
17
|
+
const APPROVAL_PADDING_MIN_ROWS = 30;
|
|
18
|
+
const APPROVAL_SCROLLBAR_COLUMNS = 2;
|
|
19
|
+
const APPROVAL_ACCENT_COLOR = 'cyan';
|
|
20
|
+
const SUDO_PASSWORD_ASSURANCE = 'The model never sees this. Your password goes straight to sudo on this ' +
|
|
21
|
+
'computer, then is discarded — never sent to our servers, saved, or logged.';
|
|
22
|
+
const SUDO_ASSURANCE_COLOR = 'ansi256(248)';
|
|
23
|
+
const APPROVAL_SCROLL_STATUS_COLOR = 'ansi256(248)';
|
|
24
|
+
const APPROVAL_SCROLLBAR_THUMB = '█';
|
|
25
|
+
const APPROVAL_SCROLLBAR_TRACK = '░';
|
|
13
26
|
const THINKING_NOTE_PREVIEW_ROWS = 3;
|
|
14
27
|
const COMPOSER_INPUT_MAX_ROWS = 6;
|
|
15
28
|
const AGENT_MODE_LABEL_WIDTH = 16;
|
|
16
29
|
const OVERLAY_PANEL_MAX_WIDTH = 86;
|
|
17
30
|
const OVERLAY_PANEL_MARGIN_LINES = 2;
|
|
31
|
+
const OVERLAY_PANEL_MARGIN_MIN_ROWS = 30;
|
|
18
32
|
const OVERLAY_BORDER_COLOR = 'yellow';
|
|
19
33
|
const OVERLAY_WARNING_COLOR = 'ansi256(208)';
|
|
20
34
|
const MODEL_PICKER_PANEL_MAX_WIDTH = 144;
|
|
@@ -238,13 +252,35 @@ function formatRelativeTime(isoDate) {
|
|
|
238
252
|
return `${hours}h ago`;
|
|
239
253
|
return `${Math.floor(hours / 24)}d ago`;
|
|
240
254
|
}
|
|
241
|
-
|
|
255
|
+
const TODO_IN_PROGRESS_FRAMES = [
|
|
256
|
+
'\u25CB',
|
|
257
|
+
'\u25D4',
|
|
258
|
+
'\u25D1',
|
|
259
|
+
'\u25D5',
|
|
260
|
+
'\u25CF',
|
|
261
|
+
];
|
|
262
|
+
const TODO_IN_PROGRESS_STATIC_GLYPH = '\u25D1';
|
|
263
|
+
export const TODO_IN_PROGRESS_STEP_MS = 1_500;
|
|
264
|
+
export function todoProgressTick(nowMs) {
|
|
265
|
+
return Math.floor(nowMs / TODO_IN_PROGRESS_STEP_MS);
|
|
266
|
+
}
|
|
267
|
+
function todoInProgressGlyph(progressTick) {
|
|
268
|
+
if (progressTick === null)
|
|
269
|
+
return TODO_IN_PROGRESS_STATIC_GLYPH;
|
|
270
|
+
const index = ((progressTick % TODO_IN_PROGRESS_FRAMES.length) +
|
|
271
|
+
TODO_IN_PROGRESS_FRAMES.length) %
|
|
272
|
+
TODO_IN_PROGRESS_FRAMES.length;
|
|
273
|
+
return TODO_IN_PROGRESS_FRAMES[index];
|
|
274
|
+
}
|
|
275
|
+
function todoItemLine(item, width, progressTick = null) {
|
|
242
276
|
const text = fitLine(item.text, Math.max(8, width - 5));
|
|
243
277
|
if (item.status === 'completed') {
|
|
244
278
|
return line(span(' ✔ ', { color: 'green' }), span(text, { color: 'gray', dim: true }));
|
|
245
279
|
}
|
|
246
280
|
if (item.status === 'in_progress') {
|
|
247
|
-
return line(span(
|
|
281
|
+
return line(span(` ${todoInProgressGlyph(progressTick)} `, {
|
|
282
|
+
color: TODO_IN_PROGRESS_COLOR,
|
|
283
|
+
}), span(text, { color: TODO_IN_PROGRESS_COLOR, bold: true }));
|
|
248
284
|
}
|
|
249
285
|
return line(span(' ○ ', { color: 'gray' }), span(text));
|
|
250
286
|
}
|
|
@@ -252,7 +288,7 @@ export function formatTodoProgress(items) {
|
|
|
252
288
|
const done = items.filter((item) => item.status === 'completed').length;
|
|
253
289
|
return `${done}/${items.length} done`;
|
|
254
290
|
}
|
|
255
|
-
export function renderTodoListLines(items, width, { header = false } = {}) {
|
|
291
|
+
export function renderTodoListLines(items, width, { header = false, progressTick = null } = {}) {
|
|
256
292
|
if (items.length === 0)
|
|
257
293
|
return [];
|
|
258
294
|
const lines = [];
|
|
@@ -277,7 +313,7 @@ export function renderTodoListLines(items, width, { header = false } = {}) {
|
|
|
277
313
|
const budget = itemRowBudget - (collapsedDone > 0 ? 1 : 0);
|
|
278
314
|
const visible = remaining.length > budget ? remaining.slice(0, budget - 1) : remaining;
|
|
279
315
|
for (const item of visible) {
|
|
280
|
-
lines.push(todoItemLine(item, width));
|
|
316
|
+
lines.push(todoItemLine(item, width, progressTick));
|
|
281
317
|
}
|
|
282
318
|
if (remaining.length > visible.length) {
|
|
283
319
|
lines.push(plainLine(` … +${remaining.length - visible.length} more`, {
|
|
@@ -296,18 +332,23 @@ export function renderTranscriptEntryLines(entry, width) {
|
|
|
296
332
|
? renderPreformattedBodyLines(entry.body, width, entry.kind)
|
|
297
333
|
: renderFormattedBodyLines(entry.body, width, entry.kind)));
|
|
298
334
|
if (entry.diffPreview) {
|
|
299
|
-
lines.push(
|
|
300
|
-
for (const diffLine of entry.diffPreview.lines.slice(0, TRANSCRIPT_DIFF_PREVIEW_LINES)) {
|
|
301
|
-
lines.push(line(span(`${diffLinePrefix(diffLine.kind)} `, { color: diffLineColor(diffLine.kind) }), span(fitLine(diffLine.content || ' ', width - 4), {
|
|
302
|
-
color: diffLineColor(diffLine.kind),
|
|
303
|
-
})));
|
|
304
|
-
}
|
|
335
|
+
lines.push(...renderDiffPreviewLines(entry.diffPreview, width));
|
|
305
336
|
}
|
|
306
337
|
if (entry.todoList && entry.todoList.length > 0) {
|
|
307
338
|
lines.push(...renderTodoListLines(entry.todoList, width));
|
|
308
339
|
}
|
|
309
340
|
return lines;
|
|
310
341
|
}
|
|
342
|
+
function renderDiffPreviewLines(preview, width, maxDiffLines = TRANSCRIPT_DIFF_PREVIEW_LINES) {
|
|
343
|
+
return [
|
|
344
|
+
plainLine(` Added ${preview.added} line${preview.added === 1 ? '' : 's'}, removed ${preview.removed} line${preview.removed === 1 ? '' : 's'}`, { color: 'gray' }),
|
|
345
|
+
...preview.lines.slice(0, maxDiffLines).map((diffLine) => line(span(`${diffLinePrefix(diffLine.kind)} `, {
|
|
346
|
+
color: diffLineColor(diffLine.kind),
|
|
347
|
+
}), span(fitLine(diffLine.content || ' ', width - 4), {
|
|
348
|
+
color: diffLineColor(diffLine.kind),
|
|
349
|
+
}))),
|
|
350
|
+
];
|
|
351
|
+
}
|
|
311
352
|
function tokenUsageLines(usage) {
|
|
312
353
|
const match = usage.match(/^Session tokens • in ([^•]+) • out ([^•]+)(?: • think ([^•]+))? • cache ([^•]+)(?: • write ([^•]+))?(?: • index ([^•]+))?$/);
|
|
313
354
|
if (!match) {
|
|
@@ -512,17 +553,48 @@ function buildJobsPickerLines(state, width, nowMs) {
|
|
|
512
553
|
}));
|
|
513
554
|
return lines;
|
|
514
555
|
}
|
|
556
|
+
export const THINKING_FALLBACK_PHRASES = [
|
|
557
|
+
'Working out where to start...',
|
|
558
|
+
'Deciding what comes first...',
|
|
559
|
+
'Lining up the pieces...',
|
|
560
|
+
'Untangling the details...',
|
|
561
|
+
'Deciding what not to touch...',
|
|
562
|
+
'Checking what this would break...',
|
|
563
|
+
'Choosing the smaller change...',
|
|
564
|
+
'Picking the least clever option...',
|
|
565
|
+
'Resisting the obvious answer...',
|
|
566
|
+
'Trying the boring explanation first...',
|
|
567
|
+
'Checking whether the assumption holds...',
|
|
568
|
+
'Asking what would have to be true...',
|
|
569
|
+
'Reading it the way the machine would...',
|
|
570
|
+
'Testing the story against the code...',
|
|
571
|
+
'Working out what actually changed...',
|
|
572
|
+
'Finding the smallest thing that explains it...',
|
|
573
|
+
'Looking for the part that is not settled yet...',
|
|
574
|
+
'Making sure this is the simple version...',
|
|
575
|
+
];
|
|
576
|
+
export function pickThinkingFallbackPhrase() {
|
|
577
|
+
const index = Math.floor(Math.random() * THINKING_FALLBACK_PHRASES.length);
|
|
578
|
+
return THINKING_FALLBACK_PHRASES[index];
|
|
579
|
+
}
|
|
580
|
+
function withProgressEllipsis(title) {
|
|
581
|
+
const text = title.trim();
|
|
582
|
+
if (!text)
|
|
583
|
+
return text;
|
|
584
|
+
return /(?:\.\.\.|…)$/.test(text) ? text : `${text}...`;
|
|
585
|
+
}
|
|
515
586
|
function thinkingHeaderLine(spinnerFrame, title, width) {
|
|
516
587
|
const spinner = `${BRAILLE_SPINNER_FRAMES[spinnerFrame % BRAILLE_SPINNER_FRAMES.length]} `;
|
|
517
|
-
const
|
|
518
|
-
|
|
588
|
+
const decorated = withProgressEllipsis(title);
|
|
589
|
+
const fittedTitle = decorated
|
|
590
|
+
? fitLine(decorated, Math.max(8, width - spinner.length - 'Thinking'.length - 3))
|
|
519
591
|
: '';
|
|
520
592
|
return line(span(spinner, { color: 'green' }), span('Thinking', { color: 'green', bold: true }), ...(fittedTitle ? [span(` · ${fittedTitle}`, { color: 'ansi256(248)' })] : []));
|
|
521
593
|
}
|
|
522
594
|
function thinkingNoteLine(note, width) {
|
|
523
595
|
return line(span('│ ', { color: 'green' }), span(fitLine(note, Math.max(8, width - 3)), { color: 'ansi256(248)' }));
|
|
524
596
|
}
|
|
525
|
-
function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
597
|
+
function buildLiveLines(state, width, spinnerFrame, elapsedSeconds, nowMs) {
|
|
526
598
|
if (!state.busy)
|
|
527
599
|
return [];
|
|
528
600
|
const lines = [plainLine('')];
|
|
@@ -552,16 +624,14 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
552
624
|
}
|
|
553
625
|
lines.push(plainLine(''));
|
|
554
626
|
}
|
|
555
|
-
|
|
556
|
-
? state.analyzingImages > 1
|
|
557
|
-
? 'Analyzing images'
|
|
558
|
-
: 'Analyzing image'
|
|
559
|
-
: 'Working';
|
|
560
|
-
lines.push(plainLine(`${WORKING_CLOCK_ICON} ${busyLabel} · ${elapsedSeconds < 60 ? `${elapsedSeconds}s` : `${Math.floor(elapsedSeconds / 60)}m ${String(elapsedSeconds % 60).padStart(2, '0')}s`}`, { color: 'yellow' }));
|
|
627
|
+
lines.push(plainLine(buildWorkingClockLine(state, elapsedSeconds), { color: 'yellow' }));
|
|
561
628
|
const todos = state.todos ?? [];
|
|
562
629
|
if (todos.length > 0) {
|
|
563
630
|
lines.push(plainLine(''));
|
|
564
|
-
lines.push(...renderTodoListLines(todos, width, {
|
|
631
|
+
lines.push(...renderTodoListLines(todos, width, {
|
|
632
|
+
header: true,
|
|
633
|
+
progressTick: todoProgressTick(nowMs),
|
|
634
|
+
}));
|
|
565
635
|
}
|
|
566
636
|
const visibleTitle = state.thinkingTitle.trim();
|
|
567
637
|
const visibleNotes = state.thinkingNotes.filter(Boolean);
|
|
@@ -571,11 +641,12 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds) {
|
|
|
571
641
|
const minimalText = visibleTitle && visibleTitle !== 'Thinking'
|
|
572
642
|
? visibleTitle
|
|
573
643
|
: (visibleNotes[visibleNotes.length - 1] ?? '');
|
|
574
|
-
lines.push(thinkingHeaderLine(spinnerFrame, minimalText, width));
|
|
644
|
+
lines.push(thinkingHeaderLine(spinnerFrame, minimalText || THINKING_FALLBACK_PHRASES[0], width));
|
|
575
645
|
}
|
|
576
646
|
else {
|
|
577
|
-
|
|
578
|
-
|
|
647
|
+
const headerTitle = visibleTitle && visibleTitle !== 'Thinking' ? visibleTitle : '';
|
|
648
|
+
lines.push(thinkingHeaderLine(spinnerFrame, headerTitle || THINKING_FALLBACK_PHRASES[0], width));
|
|
649
|
+
for (const note of visibleNotes.slice(0, THINKING_NOTE_PREVIEW_ROWS)) {
|
|
579
650
|
lines.push(thinkingNoteLine(note, width));
|
|
580
651
|
}
|
|
581
652
|
}
|
|
@@ -590,10 +661,12 @@ function overlayPanelLine(row, width, color) {
|
|
|
590
661
|
const padding = Math.max(0, width - lineCharCount(row));
|
|
591
662
|
return line(span('│ ', { color }), ...row.spans, span(' '.repeat(padding)), span(' │', { color }));
|
|
592
663
|
}
|
|
593
|
-
function buildOverlayPanel(rows, width, color) {
|
|
664
|
+
function buildOverlayPanel(rows, width, color, height = Number.POSITIVE_INFINITY) {
|
|
594
665
|
const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
|
|
595
666
|
const innerWidth = Math.max(1, panelWidth - 4);
|
|
596
|
-
const margin = Array.from({
|
|
667
|
+
const margin = Array.from({
|
|
668
|
+
length: height < OVERLAY_PANEL_MARGIN_MIN_ROWS ? 0 : OVERLAY_PANEL_MARGIN_LINES,
|
|
669
|
+
}, () => plainLine(''));
|
|
597
670
|
return [
|
|
598
671
|
...margin,
|
|
599
672
|
plainLine(`╭${'─'.repeat(panelWidth - 2)}╮`, { color }),
|
|
@@ -793,10 +866,103 @@ function buildCommandPalettePanel(suggestions, selectedIndex, width) {
|
|
|
793
866
|
body.push(modelPickerPanelSideLine(plainLine(''), innerWidth), modelPickerPanelSideLine(line(span('─'.repeat(innerWidth), { color: MODEL_PICKER_BORDER_COLOR })), innerWidth), modelPickerPanelSideLine(plainLine('↑/↓ choose • Tab or Enter accept • Esc cancel', { color: 'gray' }), innerWidth), plainLine(`╰${'─'.repeat(panelWidth - 2)}╯`, { color: MODEL_PICKER_BORDER_COLOR }));
|
|
794
867
|
return [...margin, ...body, ...margin];
|
|
795
868
|
}
|
|
869
|
+
export function formatElapsedClock(elapsedSeconds) {
|
|
870
|
+
if (elapsedSeconds < 60)
|
|
871
|
+
return `${elapsedSeconds}s`;
|
|
872
|
+
return `${Math.floor(elapsedSeconds / 60)}m ${String(elapsedSeconds % 60).padStart(2, '0')}s`;
|
|
873
|
+
}
|
|
874
|
+
export function buildWorkingClockLine(state, elapsedSeconds) {
|
|
875
|
+
const elapsed = formatElapsedClock(elapsedSeconds);
|
|
876
|
+
if (state.busyPausedAt != null) {
|
|
877
|
+
return `${WORKING_CLOCK_ICON} Paused · ${elapsed} · waiting for your response`;
|
|
878
|
+
}
|
|
879
|
+
const label = state.analyzingImages > 0
|
|
880
|
+
? state.analyzingImages > 1
|
|
881
|
+
? 'Analyzing images'
|
|
882
|
+
: 'Analyzing image'
|
|
883
|
+
: 'Working';
|
|
884
|
+
return `${WORKING_CLOCK_ICON} ${label} · ${elapsed}`;
|
|
885
|
+
}
|
|
886
|
+
export function approvalPanelInnerWidth(width) {
|
|
887
|
+
return Math.max(1, Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH)) - 4);
|
|
888
|
+
}
|
|
889
|
+
export function approvalPaddingEnabled(height) {
|
|
890
|
+
return height >= APPROVAL_PADDING_MIN_ROWS;
|
|
891
|
+
}
|
|
892
|
+
export function approvalPreviewBudget(height) {
|
|
893
|
+
const chrome = APPROVAL_OVERLAY_CHROME_ROWS +
|
|
894
|
+
(approvalPaddingEnabled(height) ? APPROVAL_PADDING_CHROME_ROWS : 0);
|
|
895
|
+
return Math.max(APPROVAL_PREVIEW_MIN_ROWS, Math.min(APPROVAL_PREVIEW_MAX_ROWS, Math.floor(height / 2) - chrome));
|
|
896
|
+
}
|
|
897
|
+
export function approvalPreviewRows(prompt, innerWidth) {
|
|
898
|
+
const previewWidth = Math.max(8, innerWidth - APPROVAL_SCROLLBAR_COLUMNS);
|
|
899
|
+
if (prompt.diffPreview) {
|
|
900
|
+
return renderDiffPreviewLines(prompt.diffPreview, previewWidth, prompt.diffPreview.lines.length);
|
|
901
|
+
}
|
|
902
|
+
return wrapText(prompt.body, previewWidth).map((text) => plainLine(text, { color: OVERLAY_BORDER_COLOR }));
|
|
903
|
+
}
|
|
904
|
+
export function approvalScrollLimit(prompt, width, height) {
|
|
905
|
+
if (!prompt)
|
|
906
|
+
return 0;
|
|
907
|
+
return Math.max(0, approvalPreviewRows(prompt, approvalPanelInnerWidth(width)).length -
|
|
908
|
+
approvalPreviewBudget(height));
|
|
909
|
+
}
|
|
910
|
+
function approvalScrollbarGlyphs(totalRows, visibleRows, offset) {
|
|
911
|
+
const thumbRows = Math.max(1, Math.min(visibleRows, Math.round((visibleRows * visibleRows) / totalRows)));
|
|
912
|
+
const maxOffset = totalRows - visibleRows;
|
|
913
|
+
const thumbTop = maxOffset <= 0
|
|
914
|
+
? 0
|
|
915
|
+
: Math.round((offset / maxOffset) * (visibleRows - thumbRows));
|
|
916
|
+
return Array.from({ length: visibleRows }, (_, index) => index >= thumbTop && index < thumbTop + thumbRows
|
|
917
|
+
? APPROVAL_SCROLLBAR_THUMB
|
|
918
|
+
: APPROVAL_SCROLLBAR_TRACK);
|
|
919
|
+
}
|
|
920
|
+
function withScrollbarGlyph(target, glyph, column) {
|
|
921
|
+
const padding = Math.max(1, column - lineCharCount(target));
|
|
922
|
+
return {
|
|
923
|
+
spans: [
|
|
924
|
+
...target.spans,
|
|
925
|
+
span(' '.repeat(padding)),
|
|
926
|
+
span(glyph, {
|
|
927
|
+
color: 'gray',
|
|
928
|
+
dim: glyph === APPROVAL_SCROLLBAR_TRACK,
|
|
929
|
+
}),
|
|
930
|
+
],
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
export function buildApprovalPreviewWindow(prompt, innerWidth, height, requestedOffset) {
|
|
934
|
+
const rows = approvalPreviewRows(prompt, innerWidth);
|
|
935
|
+
const budget = approvalPreviewBudget(height);
|
|
936
|
+
if (rows.length <= budget) {
|
|
937
|
+
return {
|
|
938
|
+
firstVisibleRow: rows.length === 0 ? 0 : 1,
|
|
939
|
+
lastVisibleRow: rows.length,
|
|
940
|
+
lines: rows,
|
|
941
|
+
offset: 0,
|
|
942
|
+
totalRows: rows.length,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
const offset = Math.max(0, Math.min(requestedOffset, rows.length - budget));
|
|
946
|
+
const visible = rows.slice(offset, offset + budget);
|
|
947
|
+
const glyphs = approvalScrollbarGlyphs(rows.length, budget, offset);
|
|
948
|
+
return {
|
|
949
|
+
firstVisibleRow: offset + 1,
|
|
950
|
+
lastVisibleRow: offset + visible.length,
|
|
951
|
+
lines: visible.map((target, index) => withScrollbarGlyph(target, glyphs[index], innerWidth - 1)),
|
|
952
|
+
offset,
|
|
953
|
+
totalRows: rows.length,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
export function approvalScrollStatusLine(window) {
|
|
957
|
+
return `lines ${window.firstVisibleRow}–${window.lastVisibleRow} of ${window.totalRows} · PgUp/PgDn scrolls`;
|
|
958
|
+
}
|
|
959
|
+
export function rightAlignedLine(text, width, style = {}) {
|
|
960
|
+
return plainLine(`${' '.repeat(Math.max(0, width - displayWidth(text)))}${text}`, style);
|
|
961
|
+
}
|
|
796
962
|
function buildOverlayLines(state, width, height, nowMs) {
|
|
797
963
|
const lines = [];
|
|
798
964
|
const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
|
|
799
|
-
const innerWidth =
|
|
965
|
+
const innerWidth = approvalPanelInnerWidth(width);
|
|
800
966
|
if (state.sudoPrompt) {
|
|
801
967
|
const prompt = state.sudoPrompt;
|
|
802
968
|
const passwordWidth = Math.max(0, innerWidth - 11);
|
|
@@ -816,53 +982,59 @@ function buildOverlayLines(state, width, height, nowMs) {
|
|
|
816
982
|
: line(span(' '), span(text, { color: OVERLAY_BORDER_COLOR })));
|
|
817
983
|
});
|
|
818
984
|
lines.push(plainLine(''));
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
}
|
|
985
|
+
lines.push(...wrapText(SUDO_PASSWORD_ASSURANCE, innerWidth).map((text) => plainLine(text, { color: SUDO_ASSURANCE_COLOR })));
|
|
986
|
+
lines.push(plainLine(''));
|
|
822
987
|
lines.push(line(span('Password: ', { color: 'cyan', bold: true }), span('•'.repeat(Math.min(prompt.passwordLength, passwordWidth)), {
|
|
823
988
|
color: 'cyan',
|
|
824
989
|
}), span(' ', { inverse: true })));
|
|
825
990
|
lines.push(plainLine('Press Enter to submit, Escape to cancel', { color: 'gray' }));
|
|
826
|
-
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR);
|
|
991
|
+
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR, height);
|
|
827
992
|
}
|
|
828
993
|
if (state.approvalPrompt) {
|
|
829
994
|
const prompt = state.approvalPrompt;
|
|
995
|
+
const padded = approvalPaddingEnabled(height);
|
|
830
996
|
lines.push(plainLine(prompt.title, {
|
|
831
|
-
color:
|
|
997
|
+
color: APPROVAL_ACCENT_COLOR,
|
|
832
998
|
bold: true,
|
|
833
999
|
}));
|
|
1000
|
+
if (padded)
|
|
1001
|
+
lines.push(plainLine(''));
|
|
834
1002
|
if (prompt.diffPreview && prompt.filePath) {
|
|
835
1003
|
lines.push(line(span('● ', { color: 'green' }), span('Update(', { bold: true }), span(prompt.filePath, { color: 'cyan', bold: true }), span(')', { bold: true })));
|
|
836
|
-
lines.push(...renderTranscriptEntryLines({
|
|
837
|
-
body: '',
|
|
838
|
-
diffPreview: prompt.diffPreview,
|
|
839
|
-
kind: 'diff',
|
|
840
|
-
title: '',
|
|
841
|
-
}, innerWidth));
|
|
842
1004
|
}
|
|
843
|
-
|
|
844
|
-
|
|
1005
|
+
const previewWindow = buildApprovalPreviewWindow(prompt, innerWidth, height, state.approvalScrollOffset ?? 0);
|
|
1006
|
+
lines.push(...previewWindow.lines);
|
|
1007
|
+
if (padded)
|
|
1008
|
+
lines.push(plainLine(''));
|
|
1009
|
+
if (previewWindow.totalRows > previewWindow.lines.length) {
|
|
1010
|
+
lines.push(rightAlignedLine(approvalScrollStatusLine(previewWindow), innerWidth, {
|
|
1011
|
+
color: APPROVAL_SCROLL_STATUS_COLOR,
|
|
1012
|
+
}));
|
|
1013
|
+
if (padded)
|
|
1014
|
+
lines.push(plainLine(''));
|
|
845
1015
|
}
|
|
846
1016
|
const options = [
|
|
847
|
-
{ value: 'y', label: 'Approve once'
|
|
848
|
-
{ value: 'a', label: 'Approve all remaining actions'
|
|
849
|
-
{ value: 'n', label: 'Deny'
|
|
1017
|
+
{ value: 'y', label: 'Approve once' },
|
|
1018
|
+
{ value: 'a', label: 'Approve all remaining actions' },
|
|
1019
|
+
{ value: 'n', label: 'Deny' },
|
|
850
1020
|
];
|
|
851
1021
|
options.forEach((option, index) => {
|
|
852
1022
|
const selected = index === state.approvalCursor;
|
|
853
1023
|
lines.push(line(span(selected ? '› ' : ' ', {
|
|
854
|
-
color: selected ?
|
|
1024
|
+
color: selected ? APPROVAL_ACCENT_COLOR : 'gray',
|
|
855
1025
|
}), span(option.value, {
|
|
856
|
-
color: selected ?
|
|
857
|
-
bold:
|
|
1026
|
+
color: selected ? APPROVAL_ACCENT_COLOR : undefined,
|
|
1027
|
+
bold: selected,
|
|
858
1028
|
}), span(` ${option.label}`, {
|
|
859
|
-
color: selected ?
|
|
1029
|
+
color: selected ? APPROVAL_ACCENT_COLOR : undefined,
|
|
860
1030
|
})));
|
|
861
1031
|
});
|
|
1032
|
+
if (padded)
|
|
1033
|
+
lines.push(plainLine(''));
|
|
862
1034
|
lines.push(plainLine('Press y, a, or n • ↑/↓ moves • Enter confirms', {
|
|
863
1035
|
color: 'gray',
|
|
864
1036
|
}));
|
|
865
|
-
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR);
|
|
1037
|
+
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR, height);
|
|
866
1038
|
}
|
|
867
1039
|
if (state.modelPickerOpen) {
|
|
868
1040
|
const options = buildModelPickerOptions(state.currentModelId, state.serverModels);
|
|
@@ -960,7 +1132,7 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
|
|
|
960
1132
|
transcriptLines.push(...block);
|
|
961
1133
|
});
|
|
962
1134
|
const sections = [];
|
|
963
|
-
const liveLines = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds);
|
|
1135
|
+
const liveLines = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds, nowMs);
|
|
964
1136
|
if (liveLines.length > 0) {
|
|
965
1137
|
sections.push({ kind: 'live', lines: liveLines });
|
|
966
1138
|
}
|
|
@@ -994,7 +1166,7 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
|
|
|
994
1166
|
sections.push({ kind: 'overlay', lines: overlayLines });
|
|
995
1167
|
}
|
|
996
1168
|
const reservedLines = countSectionLines(sections.filter((section) => section.kind !== 'transcript'));
|
|
997
|
-
const composerReserve = state.resumePickerOpen ? 0 : 4;
|
|
1169
|
+
const composerReserve = state.resumePickerOpen || state.approvalPrompt || state.sudoPrompt ? 0 : 4;
|
|
998
1170
|
const transcriptBudget = Math.max(1, rows - reservedLines - composerReserve - 1);
|
|
999
1171
|
const transcriptScrollLimit = Math.max(0, transcriptLines.length - transcriptBudget);
|
|
1000
1172
|
const transcriptScrollOffset = Math.min(Math.max(state.transcriptScrollOffset, 0), transcriptScrollLimit);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readClipboardImage, readClipboardText } from '../../core/clipboard.js';
|
|
2
2
|
import { applySlashCommandSuggestion, buildModelPickerOptions, deleteAtCursor, deleteBeforeCursor, getApprovalChoiceForCursor, getInputCommandToken, getNextApprovalCursor, getNextModelPickerIndex, getSlashCommandSuggestions, insertAtCursor, isExactSlashCommandToken, navigatePromptHistory, resolveApprovalChoiceFromInput, shouldRemountLiveFrameForComposerInputChange, } from '../repl.js';
|
|
3
3
|
import { buildPastePlaceholder, shouldCollapsePaste, } from '../paste-collapse.js';
|
|
4
|
+
const APPROVAL_PREVIEW_PAGE_ROWS = 3;
|
|
4
5
|
function isClipboardImagePasteKey(key) {
|
|
5
6
|
if (process.platform === 'win32') {
|
|
6
7
|
return ((key.ctrl || key.meta) &&
|
|
@@ -85,6 +86,31 @@ function pasteTextFromClipboard(store, handlers) {
|
|
|
85
86
|
}
|
|
86
87
|
insertPastedText(store, handlers, text);
|
|
87
88
|
}
|
|
89
|
+
function scrollTranscript(store, handlers, delta) {
|
|
90
|
+
if (delta === 0)
|
|
91
|
+
return;
|
|
92
|
+
store.update((current) => {
|
|
93
|
+
const limit = handlers.getTranscriptScrollLimit?.() ??
|
|
94
|
+
current.transcript.reduce((total, entry) => total + 2 + (entry.body ? entry.body.split('\n').length : 0), 0);
|
|
95
|
+
const next = current.transcriptScrollOffset + delta;
|
|
96
|
+
return {
|
|
97
|
+
...current,
|
|
98
|
+
transcriptScrollOffset: Math.max(0, Math.min(next, limit)),
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function scrollApprovalPreview(store, handlers, delta) {
|
|
103
|
+
if (delta === 0)
|
|
104
|
+
return;
|
|
105
|
+
store.update((current) => {
|
|
106
|
+
const limit = handlers.getApprovalScrollLimit?.() ?? 0;
|
|
107
|
+
const next = (current.approvalScrollOffset ?? 0) + delta;
|
|
108
|
+
return {
|
|
109
|
+
...current,
|
|
110
|
+
approvalScrollOffset: Math.max(0, Math.min(next, limit)),
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
88
114
|
function filterResumeSessionsLocal(sessions, filter, serverModels) {
|
|
89
115
|
const q = filter.trim().toLowerCase();
|
|
90
116
|
if (!q)
|
|
@@ -125,18 +151,7 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
125
151
|
return;
|
|
126
152
|
}
|
|
127
153
|
if (event.kind === 'transcriptScroll') {
|
|
128
|
-
|
|
129
|
-
if (delta !== 0) {
|
|
130
|
-
store.update((current) => {
|
|
131
|
-
const limit = handlers.getTranscriptScrollLimit?.() ??
|
|
132
|
-
current.transcript.reduce((total, entry) => total + 2 + (entry.body ? entry.body.split('\n').length : 0), 0);
|
|
133
|
-
const next = current.transcriptScrollOffset + delta;
|
|
134
|
-
return {
|
|
135
|
-
...current,
|
|
136
|
-
transcriptScrollOffset: Math.max(0, Math.min(next, limit)),
|
|
137
|
-
};
|
|
138
|
-
});
|
|
139
|
-
}
|
|
154
|
+
scrollTranscript(store, handlers, Math.trunc(event.deltaLines));
|
|
140
155
|
return;
|
|
141
156
|
}
|
|
142
157
|
if (event.kind !== 'key')
|
|
@@ -208,7 +223,17 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
208
223
|
return;
|
|
209
224
|
}
|
|
210
225
|
if (state.approvalPrompt) {
|
|
211
|
-
|
|
226
|
+
if (key.pageUp || key.pageDown) {
|
|
227
|
+
const delta = key.pageUp ? -APPROVAL_PREVIEW_PAGE_ROWS : APPROVAL_PREVIEW_PAGE_ROWS;
|
|
228
|
+
if (key.shift) {
|
|
229
|
+
scrollTranscript(store, handlers, key.pageUp ? 8 : -8);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
scrollApprovalPreview(store, handlers, delta);
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const directChoice = key.ctrl || key.meta ? null : resolveApprovalChoiceFromInput(key.input);
|
|
212
237
|
if (directChoice) {
|
|
213
238
|
void handlers.onResolveApproval(directChoice);
|
|
214
239
|
return;
|
|
@@ -233,15 +258,7 @@ export function handleShellKeyEvent(store, handlers, event) {
|
|
|
233
258
|
return;
|
|
234
259
|
}
|
|
235
260
|
if (key.pageUp || key.pageDown) {
|
|
236
|
-
store.
|
|
237
|
-
const transcriptLines = handlers.getTranscriptScrollLimit?.() ??
|
|
238
|
-
current.transcript.reduce((total, entry) => total + 2 + (entry.body ? entry.body.split('\n').length : 0), 0);
|
|
239
|
-
const next = current.transcriptScrollOffset + (key.pageUp ? 8 : -8);
|
|
240
|
-
return {
|
|
241
|
-
...current,
|
|
242
|
-
transcriptScrollOffset: Math.max(0, Math.min(next, transcriptLines)),
|
|
243
|
-
};
|
|
244
|
-
});
|
|
261
|
+
scrollTranscript(store, handlers, key.pageUp ? 8 : -8);
|
|
245
262
|
return;
|
|
246
263
|
}
|
|
247
264
|
if (state.jobsPickerOpen) {
|
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.13",
|
|
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.13",
|
|
41
|
+
"@thegitai/tui-darwin-x64": "1.0.0-preview.13",
|
|
42
|
+
"@thegitai/tui-linux-x64": "1.0.0-preview.13",
|
|
43
|
+
"@thegitai/tui-win32-x64": "1.0.0-preview.13",
|
|
44
44
|
"@vscode/ripgrep": "1.18.0"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|