@thegitai/cli 1.0.0-preview.12 → 1.0.0-preview.14

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.
@@ -3,6 +3,7 @@ import { singleLinePreview, truncate } from '../../utils.js';
3
3
  import { formatClientTokenUsage } from '../repl.js';
4
4
  import { renderFormattedBodyLines, renderPreformattedBodyLines, } from './markdown-render.js';
5
5
  import { displayWidth, line, plainLine, sliceToWidth, span, wrapText, } from './text.js';
6
+ import { buildUserInputOverlayLines, } from './user-input.js';
6
7
  const WORKING_CLOCK_ICON = '◷';
7
8
  const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴'];
8
9
  const TODO_PANEL_MAX_ROWS = 12;
@@ -10,12 +11,27 @@ const TODO_IN_PROGRESS_COLOR = 'ansi256(214)';
10
11
  const COMMAND_PREVIEW_LINES = 10;
11
12
  const WORKING_TOOL_PREVIEW_ROWS = 3;
12
13
  const TRANSCRIPT_DIFF_PREVIEW_LINES = 24;
14
+ const APPROVAL_PREVIEW_MAX_ROWS = 20;
15
+ const APPROVAL_PREVIEW_MIN_ROWS = 4;
16
+ const APPROVAL_OVERLAY_CHROME_ROWS = 11;
17
+ const APPROVAL_PADDING_CHROME_ROWS = 4;
18
+ const APPROVAL_PADDING_MIN_ROWS = 30;
19
+ const APPROVAL_SCROLLBAR_COLUMNS = 2;
20
+ const APPROVAL_ACCENT_COLOR = 'cyan';
21
+ const SUDO_PASSWORD_ASSURANCE = 'The model never sees this. Your password goes straight to sudo on this ' +
22
+ 'computer, then is discarded — never sent to our servers, saved, or logged.';
23
+ const SUDO_ASSURANCE_COLOR = 'ansi256(248)';
24
+ const APPROVAL_SCROLL_STATUS_COLOR = 'ansi256(248)';
25
+ const APPROVAL_SCROLLBAR_THUMB = '█';
26
+ const APPROVAL_SCROLLBAR_TRACK = '░';
13
27
  const THINKING_NOTE_PREVIEW_ROWS = 3;
14
28
  const COMPOSER_INPUT_MAX_ROWS = 6;
15
29
  const AGENT_MODE_LABEL_WIDTH = 16;
16
30
  const OVERLAY_PANEL_MAX_WIDTH = 86;
17
31
  const OVERLAY_PANEL_MARGIN_LINES = 2;
32
+ const OVERLAY_PANEL_MARGIN_MIN_ROWS = 30;
18
33
  const OVERLAY_BORDER_COLOR = 'yellow';
34
+ const USER_INPUT_BORDER_COLOR = 'cyan';
19
35
  const OVERLAY_WARNING_COLOR = 'ansi256(208)';
20
36
  const MODEL_PICKER_PANEL_MAX_WIDTH = 144;
21
37
  const MODEL_PICKER_PANEL_MARGIN_LINES = 2;
@@ -318,18 +334,23 @@ export function renderTranscriptEntryLines(entry, width) {
318
334
  ? renderPreformattedBodyLines(entry.body, width, entry.kind)
319
335
  : renderFormattedBodyLines(entry.body, width, entry.kind)));
320
336
  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
- }
337
+ lines.push(...renderDiffPreviewLines(entry.diffPreview, width));
327
338
  }
328
339
  if (entry.todoList && entry.todoList.length > 0) {
329
340
  lines.push(...renderTodoListLines(entry.todoList, width));
330
341
  }
331
342
  return lines;
332
343
  }
344
+ function renderDiffPreviewLines(preview, width, maxDiffLines = TRANSCRIPT_DIFF_PREVIEW_LINES) {
345
+ return [
346
+ plainLine(` Added ${preview.added} line${preview.added === 1 ? '' : 's'}, removed ${preview.removed} line${preview.removed === 1 ? '' : 's'}`, { color: 'gray' }),
347
+ ...preview.lines.slice(0, maxDiffLines).map((diffLine) => line(span(`${diffLinePrefix(diffLine.kind)} `, {
348
+ color: diffLineColor(diffLine.kind),
349
+ }), span(fitLine(diffLine.content || ' ', width - 4), {
350
+ color: diffLineColor(diffLine.kind),
351
+ }))),
352
+ ];
353
+ }
333
354
  function tokenUsageLines(usage) {
334
355
  const match = usage.match(/^Session tokens • in ([^•]+) • out ([^•]+)(?: • think ([^•]+))? • cache ([^•]+)(?: • write ([^•]+))?(?: • index ([^•]+))?$/);
335
356
  if (!match) {
@@ -435,13 +456,15 @@ function composerFooterLines(state) {
435
456
  : state.input
436
457
  ? 'Enter queues • Esc cancels turn • Ctrl+C clears draft'
437
458
  : 'Enter queues • Esc / Ctrl+C cancel turn';
438
- const helperText = state.busy
439
- ? busyHelperText
440
- : process.platform === 'win32'
441
- ? 'Enter sends • Shift+Tab mode • Alt+V image • Esc cancel turn • Ctrl+C clears / quits'
442
- : process.platform === 'darwin'
443
- ? 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits'
444
- : 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits';
459
+ const helperText = state.userInputPrompt
460
+ ? 'Answering agent questions • Ctrl+C cancels turn'
461
+ : state.busy
462
+ ? busyHelperText
463
+ : process.platform === 'win32'
464
+ ? 'Enter sends • Shift+Tab mode • Alt+V image • Esc cancel turn • Ctrl+C clears / quits'
465
+ : process.platform === 'darwin'
466
+ ? 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits'
467
+ : 'Enter sends • Shift+Tab mode • Ctrl+V image • Esc cancel turn • Ctrl+C clears / quits';
445
468
  const agentLabel = agentModeLabel(state.agentMode).padEnd(AGENT_MODE_LABEL_WIDTH);
446
469
  const tokenUsageText = state.tokenUsage || formatClientTokenUsage(null);
447
470
  const footerSpans = [
@@ -605,12 +628,7 @@ function buildLiveLines(state, width, spinnerFrame, elapsedSeconds, nowMs) {
605
628
  }
606
629
  lines.push(plainLine(''));
607
630
  }
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' }));
631
+ lines.push(plainLine(buildWorkingClockLine(state, elapsedSeconds), { color: 'yellow' }));
614
632
  const todos = state.todos ?? [];
615
633
  if (todos.length > 0) {
616
634
  lines.push(plainLine(''));
@@ -647,10 +665,24 @@ function overlayPanelLine(row, width, color) {
647
665
  const padding = Math.max(0, width - lineCharCount(row));
648
666
  return line(span('│ ', { color }), ...row.spans, span(' '.repeat(padding)), span(' │', { color }));
649
667
  }
650
- function buildOverlayPanel(rows, width, color) {
668
+ function overlayPanelMarginLineCount(height) {
669
+ return height < OVERLAY_PANEL_MARGIN_MIN_ROWS
670
+ ? 0
671
+ : OVERLAY_PANEL_MARGIN_LINES;
672
+ }
673
+ function overlayPanelContentBudget(height) {
674
+ if (!Number.isFinite(height)) {
675
+ return Number.POSITIVE_INFINITY;
676
+ }
677
+ const margins = overlayPanelMarginLineCount(height) * 2;
678
+ return Math.max(1, Math.floor(height) - margins - 2);
679
+ }
680
+ function buildOverlayPanel(rows, width, color, height = Number.POSITIVE_INFINITY) {
651
681
  const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
652
682
  const innerWidth = Math.max(1, panelWidth - 4);
653
- const margin = Array.from({ length: OVERLAY_PANEL_MARGIN_LINES }, () => plainLine(''));
683
+ const margin = Array.from({
684
+ length: overlayPanelMarginLineCount(height),
685
+ }, () => plainLine(''));
654
686
  return [
655
687
  ...margin,
656
688
  plainLine(`╭${'─'.repeat(panelWidth - 2)}╮`, { color }),
@@ -850,10 +882,103 @@ function buildCommandPalettePanel(suggestions, selectedIndex, width) {
850
882
  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
883
  return [...margin, ...body, ...margin];
852
884
  }
885
+ export function formatElapsedClock(elapsedSeconds) {
886
+ if (elapsedSeconds < 60)
887
+ return `${elapsedSeconds}s`;
888
+ return `${Math.floor(elapsedSeconds / 60)}m ${String(elapsedSeconds % 60).padStart(2, '0')}s`;
889
+ }
890
+ export function buildWorkingClockLine(state, elapsedSeconds) {
891
+ const elapsed = formatElapsedClock(elapsedSeconds);
892
+ if (state.busyPausedAt != null) {
893
+ return `${WORKING_CLOCK_ICON} Paused · ${elapsed} · waiting for your response`;
894
+ }
895
+ const label = state.analyzingImages > 0
896
+ ? state.analyzingImages > 1
897
+ ? 'Analyzing images'
898
+ : 'Analyzing image'
899
+ : 'Working';
900
+ return `${WORKING_CLOCK_ICON} ${label} · ${elapsed}`;
901
+ }
902
+ export function approvalPanelInnerWidth(width) {
903
+ return Math.max(1, Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH)) - 4);
904
+ }
905
+ export function approvalPaddingEnabled(height) {
906
+ return height >= APPROVAL_PADDING_MIN_ROWS;
907
+ }
908
+ export function approvalPreviewBudget(height) {
909
+ const chrome = APPROVAL_OVERLAY_CHROME_ROWS +
910
+ (approvalPaddingEnabled(height) ? APPROVAL_PADDING_CHROME_ROWS : 0);
911
+ return Math.max(APPROVAL_PREVIEW_MIN_ROWS, Math.min(APPROVAL_PREVIEW_MAX_ROWS, Math.floor(height / 2) - chrome));
912
+ }
913
+ export function approvalPreviewRows(prompt, innerWidth) {
914
+ const previewWidth = Math.max(8, innerWidth - APPROVAL_SCROLLBAR_COLUMNS);
915
+ if (prompt.diffPreview) {
916
+ return renderDiffPreviewLines(prompt.diffPreview, previewWidth, prompt.diffPreview.lines.length);
917
+ }
918
+ return wrapText(prompt.body, previewWidth).map((text) => plainLine(text, { color: OVERLAY_BORDER_COLOR }));
919
+ }
920
+ export function approvalScrollLimit(prompt, width, height) {
921
+ if (!prompt)
922
+ return 0;
923
+ return Math.max(0, approvalPreviewRows(prompt, approvalPanelInnerWidth(width)).length -
924
+ approvalPreviewBudget(height));
925
+ }
926
+ function approvalScrollbarGlyphs(totalRows, visibleRows, offset) {
927
+ const thumbRows = Math.max(1, Math.min(visibleRows, Math.round((visibleRows * visibleRows) / totalRows)));
928
+ const maxOffset = totalRows - visibleRows;
929
+ const thumbTop = maxOffset <= 0
930
+ ? 0
931
+ : Math.round((offset / maxOffset) * (visibleRows - thumbRows));
932
+ return Array.from({ length: visibleRows }, (_, index) => index >= thumbTop && index < thumbTop + thumbRows
933
+ ? APPROVAL_SCROLLBAR_THUMB
934
+ : APPROVAL_SCROLLBAR_TRACK);
935
+ }
936
+ function withScrollbarGlyph(target, glyph, column) {
937
+ const padding = Math.max(1, column - lineCharCount(target));
938
+ return {
939
+ spans: [
940
+ ...target.spans,
941
+ span(' '.repeat(padding)),
942
+ span(glyph, {
943
+ color: 'gray',
944
+ dim: glyph === APPROVAL_SCROLLBAR_TRACK,
945
+ }),
946
+ ],
947
+ };
948
+ }
949
+ export function buildApprovalPreviewWindow(prompt, innerWidth, height, requestedOffset) {
950
+ const rows = approvalPreviewRows(prompt, innerWidth);
951
+ const budget = approvalPreviewBudget(height);
952
+ if (rows.length <= budget) {
953
+ return {
954
+ firstVisibleRow: rows.length === 0 ? 0 : 1,
955
+ lastVisibleRow: rows.length,
956
+ lines: rows,
957
+ offset: 0,
958
+ totalRows: rows.length,
959
+ };
960
+ }
961
+ const offset = Math.max(0, Math.min(requestedOffset, rows.length - budget));
962
+ const visible = rows.slice(offset, offset + budget);
963
+ const glyphs = approvalScrollbarGlyphs(rows.length, budget, offset);
964
+ return {
965
+ firstVisibleRow: offset + 1,
966
+ lastVisibleRow: offset + visible.length,
967
+ lines: visible.map((target, index) => withScrollbarGlyph(target, glyphs[index], innerWidth - 1)),
968
+ offset,
969
+ totalRows: rows.length,
970
+ };
971
+ }
972
+ export function approvalScrollStatusLine(window) {
973
+ return `lines ${window.firstVisibleRow}–${window.lastVisibleRow} of ${window.totalRows} · PgUp/PgDn scrolls`;
974
+ }
975
+ export function rightAlignedLine(text, width, style = {}) {
976
+ return plainLine(`${' '.repeat(Math.max(0, width - displayWidth(text)))}${text}`, style);
977
+ }
853
978
  function buildOverlayLines(state, width, height, nowMs) {
854
979
  const lines = [];
855
980
  const panelWidth = Math.max(24, Math.min(width, OVERLAY_PANEL_MAX_WIDTH));
856
- const innerWidth = Math.max(1, panelWidth - 4);
981
+ const innerWidth = approvalPanelInnerWidth(width);
857
982
  if (state.sudoPrompt) {
858
983
  const prompt = state.sudoPrompt;
859
984
  const passwordWidth = Math.max(0, innerWidth - 11);
@@ -873,53 +998,65 @@ function buildOverlayLines(state, width, height, nowMs) {
873
998
  : line(span(' '), span(text, { color: OVERLAY_BORDER_COLOR })));
874
999
  });
875
1000
  lines.push(plainLine(''));
876
- if (prompt.prompt.trim()) {
877
- lines.push(plainLine(prompt.prompt.trim(), { color: 'gray' }));
878
- }
1001
+ lines.push(...wrapText(SUDO_PASSWORD_ASSURANCE, innerWidth).map((text) => plainLine(text, { color: SUDO_ASSURANCE_COLOR })));
1002
+ lines.push(plainLine(''));
879
1003
  lines.push(line(span('Password: ', { color: 'cyan', bold: true }), span('•'.repeat(Math.min(prompt.passwordLength, passwordWidth)), {
880
1004
  color: 'cyan',
881
1005
  }), span(' ', { inverse: true })));
882
1006
  lines.push(plainLine('Press Enter to submit, Escape to cancel', { color: 'gray' }));
883
- return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR);
1007
+ return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR, height);
1008
+ }
1009
+ if (state.userInputPrompt) {
1010
+ if (height < 3) {
1011
+ return [];
1012
+ }
1013
+ return buildOverlayPanel(buildUserInputOverlayLines(state.userInputPrompt, innerWidth, overlayPanelContentBudget(height)), width, USER_INPUT_BORDER_COLOR, height);
884
1014
  }
885
1015
  if (state.approvalPrompt) {
886
1016
  const prompt = state.approvalPrompt;
1017
+ const padded = approvalPaddingEnabled(height);
887
1018
  lines.push(plainLine(prompt.title, {
888
- color: OVERLAY_BORDER_COLOR,
1019
+ color: APPROVAL_ACCENT_COLOR,
889
1020
  bold: true,
890
1021
  }));
1022
+ if (padded)
1023
+ lines.push(plainLine(''));
891
1024
  if (prompt.diffPreview && prompt.filePath) {
892
1025
  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
1026
  }
900
- else {
901
- lines.push(...wrapText(prompt.body, innerWidth).map((text) => plainLine(text, { color: OVERLAY_BORDER_COLOR })));
1027
+ const previewWindow = buildApprovalPreviewWindow(prompt, innerWidth, height, state.approvalScrollOffset ?? 0);
1028
+ lines.push(...previewWindow.lines);
1029
+ if (padded)
1030
+ lines.push(plainLine(''));
1031
+ if (previewWindow.totalRows > previewWindow.lines.length) {
1032
+ lines.push(rightAlignedLine(approvalScrollStatusLine(previewWindow), innerWidth, {
1033
+ color: APPROVAL_SCROLL_STATUS_COLOR,
1034
+ }));
1035
+ if (padded)
1036
+ lines.push(plainLine(''));
902
1037
  }
903
1038
  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' },
1039
+ { value: 'y', label: 'Approve once' },
1040
+ { value: 'a', label: 'Approve all remaining actions' },
1041
+ { value: 'n', label: 'Deny' },
907
1042
  ];
908
1043
  options.forEach((option, index) => {
909
1044
  const selected = index === state.approvalCursor;
910
1045
  lines.push(line(span(selected ? '› ' : ' ', {
911
- color: selected ? OVERLAY_BORDER_COLOR : 'gray',
1046
+ color: selected ? APPROVAL_ACCENT_COLOR : 'gray',
912
1047
  }), span(option.value, {
913
- color: selected ? OVERLAY_BORDER_COLOR : option.color,
914
- bold: true,
1048
+ color: selected ? APPROVAL_ACCENT_COLOR : undefined,
1049
+ bold: selected,
915
1050
  }), span(` ${option.label}`, {
916
- color: selected ? OVERLAY_BORDER_COLOR : undefined,
1051
+ color: selected ? APPROVAL_ACCENT_COLOR : undefined,
917
1052
  })));
918
1053
  });
1054
+ if (padded)
1055
+ lines.push(plainLine(''));
919
1056
  lines.push(plainLine('Press y, a, or n • ↑/↓ moves • Enter confirms', {
920
1057
  color: 'gray',
921
1058
  }));
922
- return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR);
1059
+ return buildOverlayPanel(lines, width, OVERLAY_BORDER_COLOR, height);
923
1060
  }
924
1061
  if (state.modelPickerOpen) {
925
1062
  const options = buildModelPickerOptions(state.currentModelId, state.serverModels);
@@ -996,6 +1133,15 @@ function buildOverlayLines(state, width, height, nowMs) {
996
1133
  function countSectionLines(sections) {
997
1134
  return sections.reduce((sum, section) => sum + section.lines.length, 0);
998
1135
  }
1136
+ export function userInputViewportForFrame(state, cols, rows, spinnerFrame, elapsedSeconds, nowMs = 0) {
1137
+ const contentWidth = Math.max(20, Math.floor(cols * 0.95) - 2);
1138
+ const liveRows = buildLiveLines(state, contentWidth, spinnerFrame, elapsedSeconds, nowMs).length;
1139
+ const overlayHeight = Math.max(0, rows - liveRows - composerFooterLines(state).length);
1140
+ return {
1141
+ width: approvalPanelInnerWidth(contentWidth),
1142
+ maxRows: overlayPanelContentBudget(overlayHeight),
1143
+ };
1144
+ }
999
1145
  function sliceTranscriptLines(lines, maxLines, scrollOffset) {
1000
1146
  if (maxLines <= 0 || lines.length <= maxLines) {
1001
1147
  return lines;
@@ -1021,7 +1167,7 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
1021
1167
  if (liveLines.length > 0) {
1022
1168
  sections.push({ kind: 'live', lines: liveLines });
1023
1169
  }
1024
- const overlayActive = Boolean(state.approvalPrompt || state.sudoPrompt);
1170
+ const overlayActive = Boolean(state.approvalPrompt || state.sudoPrompt || state.userInputPrompt);
1025
1171
  if (!state.resumePickerOpen && !state.modelPickerOpen && !state.jobsPickerOpen && !overlayActive) {
1026
1172
  const composerLines = [];
1027
1173
  if (state.busy && state.status === 'Starting a new conversation...') {
@@ -1046,12 +1192,26 @@ export function buildTuiFrame(state, cols, rows, spinnerFrame, elapsedSeconds, n
1046
1192
  lines: [...composerLines, plainLine(''), ...composerFooterLines(state)],
1047
1193
  });
1048
1194
  }
1049
- const overlayLines = buildOverlayLines(state, contentWidth, rows, nowMs);
1195
+ if (state.userInputPrompt) {
1196
+ sections.push({
1197
+ kind: 'busyFooter',
1198
+ lines: composerFooterLines(state),
1199
+ });
1200
+ }
1201
+ const overlayHeight = state.userInputPrompt
1202
+ ? Math.max(0, rows - countSectionLines(sections))
1203
+ : rows;
1204
+ const overlayLines = buildOverlayLines(state, contentWidth, overlayHeight, nowMs);
1050
1205
  if (overlayLines.length > 0) {
1051
1206
  sections.push({ kind: 'overlay', lines: overlayLines });
1052
1207
  }
1053
1208
  const reservedLines = countSectionLines(sections.filter((section) => section.kind !== 'transcript'));
1054
- const composerReserve = state.resumePickerOpen ? 0 : 4;
1209
+ const composerReserve = state.resumePickerOpen ||
1210
+ state.approvalPrompt ||
1211
+ state.sudoPrompt ||
1212
+ state.userInputPrompt
1213
+ ? 0
1214
+ : 4;
1055
1215
  const transcriptBudget = Math.max(1, rows - reservedLines - composerReserve - 1);
1056
1216
  const transcriptScrollLimit = Math.max(0, transcriptLines.length - transcriptBudget);
1057
1217
  const transcriptScrollOffset = Math.min(Math.max(state.transcriptScrollOffset, 0), transcriptScrollLimit);
@@ -1,6 +1,8 @@
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
+ import { handleUserInputPromptEvent, } from './user-input.js';
5
+ const APPROVAL_PREVIEW_PAGE_ROWS = 3;
4
6
  function isClipboardImagePasteKey(key) {
5
7
  if (process.platform === 'win32') {
6
8
  return ((key.ctrl || key.meta) &&
@@ -9,6 +11,20 @@ function isClipboardImagePasteKey(key) {
9
11
  }
10
12
  return key.ctrl && key.input === 'v' && !key.shift && !key.meta;
11
13
  }
14
+ function applyUserInputPromptEvent(store, handlers, event) {
15
+ const current = store.getState();
16
+ if (!current.userInputPrompt)
17
+ return false;
18
+ const outcome = handleUserInputPromptEvent(current.userInputPrompt, event, handlers.getUserInputViewport?.());
19
+ store.update((state) => ({
20
+ ...state,
21
+ userInputPrompt: outcome.state,
22
+ }));
23
+ if (outcome.result) {
24
+ void handlers.onResolveUserInput?.(outcome.result);
25
+ }
26
+ return true;
27
+ }
12
28
  function composerIsEmpty(state) {
13
29
  return (!state.input &&
14
30
  state.cursor === 0 &&
@@ -85,6 +101,31 @@ function pasteTextFromClipboard(store, handlers) {
85
101
  }
86
102
  insertPastedText(store, handlers, text);
87
103
  }
104
+ function scrollTranscript(store, handlers, delta) {
105
+ if (delta === 0)
106
+ return;
107
+ store.update((current) => {
108
+ const limit = handlers.getTranscriptScrollLimit?.() ??
109
+ current.transcript.reduce((total, entry) => total + 2 + (entry.body ? entry.body.split('\n').length : 0), 0);
110
+ const next = current.transcriptScrollOffset + delta;
111
+ return {
112
+ ...current,
113
+ transcriptScrollOffset: Math.max(0, Math.min(next, limit)),
114
+ };
115
+ });
116
+ }
117
+ function scrollApprovalPreview(store, handlers, delta) {
118
+ if (delta === 0)
119
+ return;
120
+ store.update((current) => {
121
+ const limit = handlers.getApprovalScrollLimit?.() ?? 0;
122
+ const next = (current.approvalScrollOffset ?? 0) + delta;
123
+ return {
124
+ ...current,
125
+ approvalScrollOffset: Math.max(0, Math.min(next, limit)),
126
+ };
127
+ });
128
+ }
88
129
  function filterResumeSessionsLocal(sessions, filter, serverModels) {
89
130
  const q = filter.trim().toLowerCase();
90
131
  if (!q)
@@ -99,6 +140,12 @@ function filterResumeSessionsLocal(sessions, filter, serverModels) {
99
140
  }
100
141
  export function handleShellKeyEvent(store, handlers, event) {
101
142
  if (event.kind === 'paste') {
143
+ if (applyUserInputPromptEvent(store, handlers, {
144
+ kind: 'paste',
145
+ text: event.text,
146
+ })) {
147
+ return;
148
+ }
102
149
  insertPastedText(store, handlers, event.text);
103
150
  return;
104
151
  }
@@ -121,22 +168,18 @@ export function handleShellKeyEvent(store, handlers, event) {
121
168
  return;
122
169
  }
123
170
  if (event.kind === 'contextMenu') {
171
+ if (store.getState().userInputPrompt) {
172
+ const text = (handlers.readClipboardText ?? readClipboardText)();
173
+ if (text) {
174
+ applyUserInputPromptEvent(store, handlers, { kind: 'paste', text });
175
+ }
176
+ return;
177
+ }
124
178
  pasteTextFromClipboard(store, handlers);
125
179
  return;
126
180
  }
127
181
  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
- }
182
+ scrollTranscript(store, handlers, Math.trunc(event.deltaLines));
140
183
  return;
141
184
  }
142
185
  if (event.kind !== 'key')
@@ -153,6 +196,10 @@ export function handleShellKeyEvent(store, handlers, event) {
153
196
  }
154
197
  };
155
198
  if (key.ctrl && key.input === 'c') {
199
+ if (state.userInputPrompt) {
200
+ handlers.onCtrlC?.();
201
+ return;
202
+ }
156
203
  if (state.sudoPrompt) {
157
204
  handlers.onSudoPasswordInput({ kind: 'cancel' });
158
205
  return;
@@ -185,6 +232,10 @@ export function handleShellKeyEvent(store, handlers, event) {
185
232
  handlers.onRequestExit();
186
233
  return;
187
234
  }
235
+ if (state.userInputPrompt &&
236
+ applyUserInputPromptEvent(store, handlers, key)) {
237
+ return;
238
+ }
188
239
  if (state.sudoPrompt) {
189
240
  if (key.escape) {
190
241
  handlers.onSudoPasswordInput({ kind: 'cancel' });
@@ -208,7 +259,17 @@ export function handleShellKeyEvent(store, handlers, event) {
208
259
  return;
209
260
  }
210
261
  if (state.approvalPrompt) {
211
- const directChoice = resolveApprovalChoiceFromInput(key.input);
262
+ if (key.pageUp || key.pageDown) {
263
+ const delta = key.pageUp ? -APPROVAL_PREVIEW_PAGE_ROWS : APPROVAL_PREVIEW_PAGE_ROWS;
264
+ if (key.shift) {
265
+ scrollTranscript(store, handlers, key.pageUp ? 8 : -8);
266
+ }
267
+ else {
268
+ scrollApprovalPreview(store, handlers, delta);
269
+ }
270
+ return;
271
+ }
272
+ const directChoice = key.ctrl || key.meta ? null : resolveApprovalChoiceFromInput(key.input);
212
273
  if (directChoice) {
213
274
  void handlers.onResolveApproval(directChoice);
214
275
  return;
@@ -233,15 +294,7 @@ export function handleShellKeyEvent(store, handlers, event) {
233
294
  return;
234
295
  }
235
296
  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
- });
297
+ scrollTranscript(store, handlers, key.pageUp ? 8 : -8);
245
298
  return;
246
299
  }
247
300
  if (state.jobsPickerOpen) {