@thegitai/cli 1.0.0-preview.12 → 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/executor.js +1 -1
- package/dist/src/ui/repl.js +55 -14
- package/dist/src/ui/tui/build-frame.js +152 -37
- package/dist/src/ui/tui/shell-input.js +39 -22
- package/package.json +5 -5
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, pickThinkingFallbackPhrase, renderTranscriptEntryLines, THINKING_FALLBACK_PHRASES, } 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,
|
|
@@ -1265,6 +1267,30 @@ export function resolveApprovalChoiceFromInput(input) {
|
|
|
1265
1267
|
return 'n';
|
|
1266
1268
|
return null;
|
|
1267
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
|
+
}
|
|
1268
1294
|
export function buildModelPickerOptions(currentModelId, serverModels) {
|
|
1269
1295
|
return serverModels.map((model) => ({
|
|
1270
1296
|
id: model.id,
|
|
@@ -1419,9 +1445,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1419
1445
|
return;
|
|
1420
1446
|
const state = store.getState();
|
|
1421
1447
|
syncTerminalTitle();
|
|
1422
|
-
const elapsedSeconds = state.
|
|
1423
|
-
? Math.max(0, Math.floor((Date.now() - state.busySince) / 1000))
|
|
1424
|
-
: 0;
|
|
1448
|
+
const elapsedSeconds = busyElapsedSeconds(state, Date.now());
|
|
1425
1449
|
bridge.render(buildTuiFrame(state, terminalCols, terminalRows, spinnerFrame, elapsedSeconds, Date.now()));
|
|
1426
1450
|
};
|
|
1427
1451
|
const remountTui = async () => {
|
|
@@ -1553,9 +1577,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1553
1577
|
resolveApprovalChoice = null;
|
|
1554
1578
|
pendingResolve('n');
|
|
1555
1579
|
store.update((current) => ({
|
|
1556
|
-
...current,
|
|
1580
|
+
...resumeBusyClock(current, Date.now()),
|
|
1557
1581
|
approvalCursor: getDefaultApprovalCursor(),
|
|
1558
1582
|
approvalPrompt: null,
|
|
1583
|
+
approvalScrollOffset: 0,
|
|
1559
1584
|
}));
|
|
1560
1585
|
};
|
|
1561
1586
|
const dismissPendingSudoPassword = () => {
|
|
@@ -1568,7 +1593,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1568
1593
|
sudoPasswordBuffer = '';
|
|
1569
1594
|
pendingResolve(null);
|
|
1570
1595
|
store.update((current) => ({
|
|
1571
|
-
...current,
|
|
1596
|
+
...resumeBusyClock(current, Date.now()),
|
|
1572
1597
|
sudoPrompt: null,
|
|
1573
1598
|
}));
|
|
1574
1599
|
};
|
|
@@ -1597,6 +1622,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1597
1622
|
activeTurnInput: '',
|
|
1598
1623
|
activeTurnInputPreformatted: false,
|
|
1599
1624
|
busy: false,
|
|
1625
|
+
busyPausedAt: null,
|
|
1600
1626
|
busySince: null,
|
|
1601
1627
|
commandLog: [],
|
|
1602
1628
|
cursor: queued ? queued.body.length : current.cursor,
|
|
@@ -1610,7 +1636,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1610
1636
|
thinkingTitle: '',
|
|
1611
1637
|
thinkingNotes: [],
|
|
1612
1638
|
workingTools: [],
|
|
1613
|
-
tokenUsage: formatClientTokenUsage(current
|
|
1639
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
1614
1640
|
}));
|
|
1615
1641
|
appendStaticEntries(cancelledEntries);
|
|
1616
1642
|
lastTurnStartedAt = null;
|
|
@@ -1761,7 +1787,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1761
1787
|
cleanupSudoPasswordPrompt = null;
|
|
1762
1788
|
}
|
|
1763
1789
|
store.update((current) => ({
|
|
1764
|
-
...current,
|
|
1790
|
+
...pauseBusyClock(current, Date.now()),
|
|
1765
1791
|
sudoPrompt: {
|
|
1766
1792
|
command,
|
|
1767
1793
|
passwordLength: 0,
|
|
@@ -1781,7 +1807,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1781
1807
|
cleanupSudoPasswordPrompt = null;
|
|
1782
1808
|
sudoPasswordBuffer = '';
|
|
1783
1809
|
store.update((current) => ({
|
|
1784
|
-
...current,
|
|
1810
|
+
...resumeBusyClock(current, Date.now()),
|
|
1785
1811
|
sudoPrompt: null,
|
|
1786
1812
|
status: current.sudoPrompt?.returnStatus ?? current.status,
|
|
1787
1813
|
}));
|
|
@@ -1795,7 +1821,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1795
1821
|
cleanupSudoPasswordPrompt = null;
|
|
1796
1822
|
sudoPasswordBuffer = '';
|
|
1797
1823
|
store.update((current) => ({
|
|
1798
|
-
...current,
|
|
1824
|
+
...resumeBusyClock(current, Date.now()),
|
|
1799
1825
|
sudoPrompt: null,
|
|
1800
1826
|
status: current.sudoPrompt?.returnStatus ?? current.status,
|
|
1801
1827
|
}));
|
|
@@ -1822,8 +1848,9 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1822
1848
|
}
|
|
1823
1849
|
resolveApprovalChoice = resolve;
|
|
1824
1850
|
store.update((current) => ({
|
|
1825
|
-
...current,
|
|
1851
|
+
...pauseBusyClock(current, Date.now()),
|
|
1826
1852
|
approvalCursor: getDefaultApprovalCursor(),
|
|
1853
|
+
approvalScrollOffset: 0,
|
|
1827
1854
|
approvalPrompt: {
|
|
1828
1855
|
title,
|
|
1829
1856
|
body,
|
|
@@ -1841,9 +1868,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
1841
1868
|
const pendingResolve = resolveApprovalChoice;
|
|
1842
1869
|
resolveApprovalChoice = null;
|
|
1843
1870
|
store.update((next) => ({
|
|
1844
|
-
...next,
|
|
1871
|
+
...resumeBusyClock(next, Date.now()),
|
|
1845
1872
|
approvalCursor: getDefaultApprovalCursor(),
|
|
1846
1873
|
approvalPrompt: null,
|
|
1874
|
+
approvalScrollOffset: 0,
|
|
1847
1875
|
status: current.approvalPrompt?.returnStatus ?? next.status,
|
|
1848
1876
|
}));
|
|
1849
1877
|
pendingResolve?.(choice);
|
|
@@ -2051,6 +2079,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2051
2079
|
store.update((next) => ({
|
|
2052
2080
|
...next,
|
|
2053
2081
|
busy: true,
|
|
2082
|
+
busyPausedAt: null,
|
|
2054
2083
|
busySince: resumeStartedAt,
|
|
2055
2084
|
clockNow: resumeStartedAt,
|
|
2056
2085
|
status: 'Loading session...',
|
|
@@ -2067,6 +2096,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2067
2096
|
store.update((next) => ({
|
|
2068
2097
|
...next,
|
|
2069
2098
|
busy: false,
|
|
2099
|
+
busyPausedAt: null,
|
|
2070
2100
|
busySince: null,
|
|
2071
2101
|
resumePickerFilter: '',
|
|
2072
2102
|
resumePickerIndex: 0,
|
|
@@ -2090,6 +2120,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2090
2120
|
store.update((next) => ({
|
|
2091
2121
|
...next,
|
|
2092
2122
|
busy: false,
|
|
2123
|
+
busyPausedAt: null,
|
|
2093
2124
|
busySince: null,
|
|
2094
2125
|
resumePickerFilter: '',
|
|
2095
2126
|
resumePickerIndex: 0,
|
|
@@ -2315,6 +2346,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2315
2346
|
store.update((current) => ({
|
|
2316
2347
|
...current,
|
|
2317
2348
|
busy: true,
|
|
2349
|
+
busyPausedAt: null,
|
|
2318
2350
|
busySince: Date.now(),
|
|
2319
2351
|
imageAttachments: [],
|
|
2320
2352
|
queuedMessage: null,
|
|
@@ -2325,6 +2357,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2325
2357
|
store.update((current) => ({
|
|
2326
2358
|
...current,
|
|
2327
2359
|
busy: false,
|
|
2360
|
+
busyPausedAt: null,
|
|
2328
2361
|
busySince: null,
|
|
2329
2362
|
status: 'Ready',
|
|
2330
2363
|
}));
|
|
@@ -2352,6 +2385,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2352
2385
|
store.update((current) => ({
|
|
2353
2386
|
...current,
|
|
2354
2387
|
busy: false,
|
|
2388
|
+
busyPausedAt: null,
|
|
2355
2389
|
busySince: null,
|
|
2356
2390
|
status: 'Ready',
|
|
2357
2391
|
}));
|
|
@@ -2384,6 +2418,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2384
2418
|
activeTurnInputPreformatted: preformatted,
|
|
2385
2419
|
analyzingImages: 0,
|
|
2386
2420
|
busy: true,
|
|
2421
|
+
busyPausedAt: null,
|
|
2387
2422
|
busySince: turnStartedAt,
|
|
2388
2423
|
clockNow: turnStartedAt,
|
|
2389
2424
|
status: 'Running turn...',
|
|
@@ -2445,6 +2480,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2445
2480
|
store.update((current) => ({
|
|
2446
2481
|
...current,
|
|
2447
2482
|
busy: false,
|
|
2483
|
+
busyPausedAt: null,
|
|
2448
2484
|
busySince: null,
|
|
2449
2485
|
status: result.waitingForApproval ? 'Awaiting approval' : 'Ready',
|
|
2450
2486
|
tokenUsage: formatClientTokenUsage(Date.now() - turnStartedAt, latestUsageSummary),
|
|
@@ -2466,6 +2502,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2466
2502
|
activeTurnInput: '',
|
|
2467
2503
|
activeTurnInputPreformatted: false,
|
|
2468
2504
|
busy: false,
|
|
2505
|
+
busyPausedAt: null,
|
|
2469
2506
|
busySince: null,
|
|
2470
2507
|
commandLog: [],
|
|
2471
2508
|
imageAttachments: [],
|
|
@@ -2517,7 +2554,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2517
2554
|
store.update((current) => ({
|
|
2518
2555
|
...current,
|
|
2519
2556
|
status: message,
|
|
2520
|
-
tokenUsage: formatClientTokenUsage(current
|
|
2557
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
2521
2558
|
}));
|
|
2522
2559
|
queueThinkingUpdate({
|
|
2523
2560
|
title: panel ? panel.title : liveStatusPanelTitle(message),
|
|
@@ -2528,7 +2565,7 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2528
2565
|
store.update((current) => ({
|
|
2529
2566
|
...current,
|
|
2530
2567
|
contextStatus: message,
|
|
2531
|
-
tokenUsage: formatClientTokenUsage(current
|
|
2568
|
+
tokenUsage: formatClientTokenUsage(busyElapsedMs(current, Date.now()), latestUsageSummary),
|
|
2532
2569
|
}));
|
|
2533
2570
|
};
|
|
2534
2571
|
session.onToolEvent = (event) => {
|
|
@@ -2637,6 +2674,10 @@ export async function runClientInteractive({ appendPromptHistory, authConfig, de
|
|
|
2637
2674
|
return choice === 'y';
|
|
2638
2675
|
};
|
|
2639
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
|
+
},
|
|
2640
2681
|
getTranscriptScrollLimit: () => {
|
|
2641
2682
|
const contentWidth = Math.max(20, Math.floor(terminalCols * 0.95) - 2);
|
|
2642
2683
|
const blocks = store.getState().transcript.map((entry) => renderTranscriptEntryLines(entry, contentWidth));
|
|
@@ -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;
|
|
@@ -318,18 +332,23 @@ export function renderTranscriptEntryLines(entry, width) {
|
|
|
318
332
|
? renderPreformattedBodyLines(entry.body, width, entry.kind)
|
|
319
333
|
: renderFormattedBodyLines(entry.body, width, entry.kind)));
|
|
320
334
|
if (entry.diffPreview) {
|
|
321
|
-
lines.push(
|
|
322
|
-
for (const diffLine of entry.diffPreview.lines.slice(0, TRANSCRIPT_DIFF_PREVIEW_LINES)) {
|
|
323
|
-
lines.push(line(span(`${diffLinePrefix(diffLine.kind)} `, { color: diffLineColor(diffLine.kind) }), span(fitLine(diffLine.content || ' ', width - 4), {
|
|
324
|
-
color: diffLineColor(diffLine.kind),
|
|
325
|
-
})));
|
|
326
|
-
}
|
|
335
|
+
lines.push(...renderDiffPreviewLines(entry.diffPreview, width));
|
|
327
336
|
}
|
|
328
337
|
if (entry.todoList && entry.todoList.length > 0) {
|
|
329
338
|
lines.push(...renderTodoListLines(entry.todoList, width));
|
|
330
339
|
}
|
|
331
340
|
return lines;
|
|
332
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
|
+
}
|
|
333
352
|
function tokenUsageLines(usage) {
|
|
334
353
|
const match = usage.match(/^Session tokens • in ([^•]+) • out ([^•]+)(?: • think ([^•]+))? • cache ([^•]+)(?: • write ([^•]+))?(?: • index ([^•]+))?$/);
|
|
335
354
|
if (!match) {
|
|
@@ -605,12 +624,7 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds, nowMs) {
|
|
|
605
624
|
}
|
|
606
625
|
lines.push(plainLine(''));
|
|
607
626
|
}
|
|
608
|
-
|
|
609
|
-
? state.analyzingImages > 1
|
|
610
|
-
? 'Analyzing images'
|
|
611
|
-
: 'Analyzing image'
|
|
612
|
-
: 'Working';
|
|
613
|
-
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' }));
|
|
614
628
|
const todos = state.todos ?? [];
|
|
615
629
|
if (todos.length > 0) {
|
|
616
630
|
lines.push(plainLine(''));
|
|
@@ -647,10 +661,12 @@ function overlayPanelLine(row, width, color) {
|
|
|
647
661
|
const padding = Math.max(0, width - lineCharCount(row));
|
|
648
662
|
return line(span('│ ', { color }), ...row.spans, span(' '.repeat(padding)), span(' │', { color }));
|
|
649
663
|
}
|
|
650
|
-
function buildOverlayPanel(rows, width, color) {
|
|
664
|
+
function buildOverlayPanel(rows, width, color, height = Number.POSITIVE_INFINITY) {
|
|
651
665
|
const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
|
|
652
666
|
const innerWidth = Math.max(1, panelWidth - 4);
|
|
653
|
-
const margin = Array.from({
|
|
667
|
+
const margin = Array.from({
|
|
668
|
+
length: height < OVERLAY_PANEL_MARGIN_MIN_ROWS ? 0 : OVERLAY_PANEL_MARGIN_LINES,
|
|
669
|
+
}, () => plainLine(''));
|
|
654
670
|
return [
|
|
655
671
|
...margin,
|
|
656
672
|
plainLine(`╭${'─'.repeat(panelWidth - 2)}╮`, { color }),
|
|
@@ -850,10 +866,103 @@ function buildCommandPalettePanel(suggestions, selectedIndex, width) {
|
|
|
850
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 }));
|
|
851
867
|
return [...margin, ...body, ...margin];
|
|
852
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
|
+
}
|
|
853
962
|
function buildOverlayLines(state, width, height, nowMs) {
|
|
854
963
|
const lines = [];
|
|
855
964
|
const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
|
|
856
|
-
const innerWidth =
|
|
965
|
+
const innerWidth = approvalPanelInnerWidth(width);
|
|
857
966
|
if (state.sudoPrompt) {
|
|
858
967
|
const prompt = state.sudoPrompt;
|
|
859
968
|
const passwordWidth = Math.max(0, innerWidth - 11);
|
|
@@ -873,53 +982,59 @@ function buildOverlayLines(state, width, height, nowMs) {
|
|
|
873
982
|
: line(span(' '), span(text, { color: OVERLAY_BORDER_COLOR })));
|
|
874
983
|
});
|
|
875
984
|
lines.push(plainLine(''));
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
}
|
|
985
|
+
lines.push(...wrapText(SUDO_PASSWORD_ASSURANCE, innerWidth).map((text) => plainLine(text, { color: SUDO_ASSURANCE_COLOR })));
|
|
986
|
+
lines.push(plainLine(''));
|
|
879
987
|
lines.push(line(span('Password: ', { color: 'cyan', bold: true }), span('•'.repeat(Math.min(prompt.passwordLength, passwordWidth)), {
|
|
880
988
|
color: 'cyan',
|
|
881
989
|
}), span(' ', { inverse: true })));
|
|
882
990
|
lines.push(plainLine('Press Enter to submit, Escape to cancel', { color: 'gray' }));
|
|
883
|
-
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR);
|
|
991
|
+
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR, height);
|
|
884
992
|
}
|
|
885
993
|
if (state.approvalPrompt) {
|
|
886
994
|
const prompt = state.approvalPrompt;
|
|
995
|
+
const padded = approvalPaddingEnabled(height);
|
|
887
996
|
lines.push(plainLine(prompt.title, {
|
|
888
|
-
color:
|
|
997
|
+
color: APPROVAL_ACCENT_COLOR,
|
|
889
998
|
bold: true,
|
|
890
999
|
}));
|
|
1000
|
+
if (padded)
|
|
1001
|
+
lines.push(plainLine(''));
|
|
891
1002
|
if (prompt.diffPreview && prompt.filePath) {
|
|
892
1003
|
lines.push(line(span('● ', { color: 'green' }), span('Update(', { bold: true }), span(prompt.filePath, { color: 'cyan', bold: true }), span(')', { bold: true })));
|
|
893
|
-
lines.push(...renderTranscriptEntryLines({
|
|
894
|
-
body: '',
|
|
895
|
-
diffPreview: prompt.diffPreview,
|
|
896
|
-
kind: 'diff',
|
|
897
|
-
title: '',
|
|
898
|
-
}, innerWidth));
|
|
899
1004
|
}
|
|
900
|
-
|
|
901
|
-
|
|
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(''));
|
|
902
1015
|
}
|
|
903
1016
|
const options = [
|
|
904
|
-
{ value: 'y', label: 'Approve once'
|
|
905
|
-
{ value: 'a', label: 'Approve all remaining actions'
|
|
906
|
-
{ value: 'n', label: 'Deny'
|
|
1017
|
+
{ value: 'y', label: 'Approve once' },
|
|
1018
|
+
{ value: 'a', label: 'Approve all remaining actions' },
|
|
1019
|
+
{ value: 'n', label: 'Deny' },
|
|
907
1020
|
];
|
|
908
1021
|
options.forEach((option, index) => {
|
|
909
1022
|
const selected = index === state.approvalCursor;
|
|
910
1023
|
lines.push(line(span(selected ? '› ' : ' ', {
|
|
911
|
-
color: selected ?
|
|
1024
|
+
color: selected ? APPROVAL_ACCENT_COLOR : 'gray',
|
|
912
1025
|
}), span(option.value, {
|
|
913
|
-
color: selected ?
|
|
914
|
-
bold:
|
|
1026
|
+
color: selected ? APPROVAL_ACCENT_COLOR : undefined,
|
|
1027
|
+
bold: selected,
|
|
915
1028
|
}), span(` ${option.label}`, {
|
|
916
|
-
color: selected ?
|
|
1029
|
+
color: selected ? APPROVAL_ACCENT_COLOR : undefined,
|
|
917
1030
|
})));
|
|
918
1031
|
});
|
|
1032
|
+
if (padded)
|
|
1033
|
+
lines.push(plainLine(''));
|
|
919
1034
|
lines.push(plainLine('Press y, a, or n • ↑/↓ moves • Enter confirms', {
|
|
920
1035
|
color: 'gray',
|
|
921
1036
|
}));
|
|
922
|
-
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR);
|
|
1037
|
+
return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR, height);
|
|
923
1038
|
}
|
|
924
1039
|
if (state.modelPickerOpen) {
|
|
925
1040
|
const options = buildModelPickerOptions(state.currentModelId, state.serverModels);
|
|
@@ -1051,7 +1166,7 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
|
|
|
1051
1166
|
sections.push({ kind: 'overlay', lines: overlayLines });
|
|
1052
1167
|
}
|
|
1053
1168
|
const reservedLines = countSectionLines(sections.filter((section) => section.kind !== 'transcript'));
|
|
1054
|
-
const composerReserve = state.resumePickerOpen ? 0 : 4;
|
|
1169
|
+
const composerReserve = state.resumePickerOpen || state.approvalPrompt || state.sudoPrompt ? 0 : 4;
|
|
1055
1170
|
const transcriptBudget = Math.max(1, rows - reservedLines - composerReserve - 1);
|
|
1056
1171
|
const transcriptScrollLimit = Math.max(0, transcriptLines.length - transcriptBudget);
|
|
1057
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": {
|