@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.
- package/README.md +16 -0
- package/dist/src/api/chat.js +52 -0
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/executor.js +1 -1
- package/dist/src/session.js +2 -1
- package/dist/src/ui/repl.js +148 -14
- package/dist/src/ui/tui/build-frame.js +206 -46
- package/dist/src/ui/tui/shell-input.js +75 -22
- package/dist/src/ui/tui/user-input.js +568 -0
- package/package.json +5 -5
|
@@ -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(
|
|
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.
|
|
439
|
-
?
|
|
440
|
-
:
|
|
441
|
-
?
|
|
442
|
-
: process.platform === '
|
|
443
|
-
? 'Enter sends • Shift+Tab mode •
|
|
444
|
-
:
|
|
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
|
-
|
|
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
|
|
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({
|
|
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 =
|
|
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
|
-
|
|
877
|
-
|
|
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:
|
|
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
|
-
|
|
901
|
-
|
|
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'
|
|
905
|
-
{ value: 'a', label: 'Approve all remaining actions'
|
|
906
|
-
{ value: 'n', label: 'Deny'
|
|
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 ?
|
|
1046
|
+
color: selected ? APPROVAL_ACCENT_COLOR : 'gray',
|
|
912
1047
|
}), span(option.value, {
|
|
913
|
-
color: selected ?
|
|
914
|
-
bold:
|
|
1048
|
+
color: selected ? APPROVAL_ACCENT_COLOR : undefined,
|
|
1049
|
+
bold: selected,
|
|
915
1050
|
}), span(` ${option.label}`, {
|
|
916
|
-
color: selected ?
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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) {
|