@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.
@@ -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]*: ?|sudo[^\r\n]*password[^\r\n]*: ?|password[^\r\n]*: ?)$/i);
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) {
@@ -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.busySince
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.busySince == null ? null : Date.now() - current.busySince, latestUsageSummary),
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.busySince == null ? null : Date.now() - current.busySince, latestUsageSummary),
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.busySince == null ? null : Date.now() - current.busySince, latestUsageSummary),
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(plainLine(` Added ${entry.diffPreview.added} line${entry.diffPreview.added === 1 ? '' : 's'}, removed ${entry.diffPreview.removed} line${entry.diffPreview.removed === 1 ? '' : 's'}`, { color: 'gray' }));
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
- const busyLabel = state.analyzingImages > 0
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({ length: OVERLAY_PANEL_MARGIN_LINES }, () => plainLine(''));
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 = Math.max(1, panelWidth - 4);
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
- if (prompt.prompt.trim()) {
877
- lines.push(plainLine(prompt.prompt.trim(), { color: 'gray' }));
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: OVERLAY_BORDER_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
- else {
901
- lines.push(...wrapText(prompt.body, innerWidth).map((text) => plainLine(text, { color: OVERLAY_BORDER_COLOR })));
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', color: 'green' },
905
- { value: 'a', label: 'Approve all remaining actions', color: 'cyan' },
906
- { value: 'n', label: 'Deny', color: 'red' },
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 ? OVERLAY_BORDER_COLOR : 'gray',
1024
+ color: selected ? APPROVAL_ACCENT_COLOR : 'gray',
912
1025
  }), span(option.value, {
913
- color: selected ? OVERLAY_BORDER_COLOR : option.color,
914
- bold: true,
1026
+ color: selected ? APPROVAL_ACCENT_COLOR : undefined,
1027
+ bold: selected,
915
1028
  }), span(` ${option.label}`, {
916
- color: selected ? OVERLAY_BORDER_COLOR : undefined,
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
- const delta = Math.trunc(event.deltaLines);
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
- const directChoice = resolveApprovalChoiceFromInput(key.input);
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.update((current) => {
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.12",
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.12",
41
- "@thegitai/tui-darwin-x64": "1.0.0-preview.12",
42
- "@thegitai/tui-linux-x64": "1.0.0-preview.12",
43
- "@thegitai/tui-win32-x64": "1.0.0-preview.12",
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": {