@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.
@@ -160,7 +160,7 @@ function publicStatusMessage(data) {
160
160
  ? event.toolName
161
161
  : 'tool';
162
162
  if (event.phase === 'thinking')
163
- return 'Thinking...';
163
+ return 'Exploring options...';
164
164
  if (event.phase === 'analyzing_image') {
165
165
  return (event.imageCount ?? 1) > 1 ? 'Analyzing images...' : 'Analyzing image...';
166
166
  }
@@ -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, 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(-3),
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.busySince
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.busySince == null ? null : Date.now() - current.busySince, latestUsageSummary),
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
- thinkingTitle: panel ? panel.title : liveStatusPanelTitle(message),
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.busySince == null ? null : Date.now() - current.busySince, latestUsageSummary),
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
- function todoItemLine(item, width) {
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(' ◐ ', { color: TODO_IN_PROGRESS_COLOR }), span(text, { color: TODO_IN_PROGRESS_COLOR, bold: true }));
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(plainLine(` Added ${entry.diffPreview.added} line${entry.diffPreview.added === 1 ? '' : 's'}, removed ${entry.diffPreview.removed} line${entry.diffPreview.removed === 1 ? '' : 's'}`, { color: 'gray' }));
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 fittedTitle = title
518
- ? fitLine(title, Math.max(8, width - spinner.length - 'Thinking'.length - 3))
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
- const busyLabel = state.analyzingImages > 0
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, { header: true }));
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
- lines.push(thinkingHeaderLine(spinnerFrame, visibleTitle, width));
578
- for (const note of visibleNotes.slice(-THINKING_NOTE_PREVIEW_ROWS)) {
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({ 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(''));
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 = Math.max(1, panelWidth - 4);
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
- if (prompt.prompt.trim()) {
820
- lines.push(plainLine(prompt.prompt.trim(), { color: 'gray' }));
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: OVERLAY_BORDER_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
- else {
844
- 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(''));
845
1015
  }
846
1016
  const options = [
847
- { value: 'y', label: 'Approve once', color: 'green' },
848
- { value: 'a', label: 'Approve all remaining actions', color: 'cyan' },
849
- { 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' },
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 ? OVERLAY_BORDER_COLOR : 'gray',
1024
+ color: selected ? APPROVAL_ACCENT_COLOR : 'gray',
855
1025
  }), span(option.value, {
856
- color: selected ? OVERLAY_BORDER_COLOR : option.color,
857
- bold: true,
1026
+ color: selected ? APPROVAL_ACCENT_COLOR : undefined,
1027
+ bold: selected,
858
1028
  }), span(` ${option.label}`, {
859
- color: selected ? OVERLAY_BORDER_COLOR : undefined,
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
- 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.11",
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.11",
41
- "@thegitai/tui-darwin-x64": "1.0.0-preview.11",
42
- "@thegitai/tui-linux-x64": "1.0.0-preview.11",
43
- "@thegitai/tui-win32-x64": "1.0.0-preview.11",
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": {