@aitherium/shell-cli 1.11.1 → 1.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +10 -0
- package/dist/client.js +62 -6
- package/dist/commands.js +153 -0
- package/dist/doc-view.d.ts +35 -0
- package/dist/doc-view.js +144 -0
- package/dist/image-preview.d.ts +45 -0
- package/dist/image-preview.js +250 -0
- package/dist/install-wizard.d.ts +94 -0
- package/dist/install-wizard.js +397 -0
- package/dist/main.js +36 -9
- package/dist/renderer.js +52 -4
- package/dist/repl.js +204 -10
- package/dist/status-banner.js +22 -2
- package/dist/tui/controller.js +37 -0
- package/dist/tui/repl-tui.js +222 -10
- package/dist/tui/screen.d.ts +12 -0
- package/dist/tui/screen.js +326 -1
- package/package.json +1 -1
package/dist/tui/screen.d.ts
CHANGED
|
@@ -33,6 +33,11 @@ export interface TuiSurface {
|
|
|
33
33
|
clearPanes(): void;
|
|
34
34
|
/** Show/hide the TRACE pane (OUTPUT widens to fill). */
|
|
35
35
|
toggleTrace(): void;
|
|
36
|
+
/** Stash the suggested follow-ups from the last turn so a bare digit in the
|
|
37
|
+
* input box can expand to "send the Nth follow-up". Cleared when consumed. */
|
|
38
|
+
setPendingFollowups(items: string[]): void;
|
|
39
|
+
/** The currently-stashed follow-ups (empty if none / already consumed). */
|
|
40
|
+
getPendingFollowups(): string[];
|
|
36
41
|
/** Coalesced repaint. */
|
|
37
42
|
render(): void;
|
|
38
43
|
/** Focus the input box for typing. */
|
|
@@ -43,6 +48,13 @@ export interface TuiSurface {
|
|
|
43
48
|
pickerOpen(): boolean;
|
|
44
49
|
/** Show a filterable command picker; resolves with the chosen value or null. */
|
|
45
50
|
showPicker(title: string, items: PickerItem[], initialFilter?: string): Promise<string | null>;
|
|
51
|
+
/** Show a full-screen scrollable document viewer over pre-rendered lines
|
|
52
|
+
* (markdown/code/text). Resolves when the user closes it (q/Esc). */
|
|
53
|
+
showViewer(title: string, lines: string[]): Promise<void>;
|
|
54
|
+
/** Show a full-screen multi-line text editor seeded with `initialText`.
|
|
55
|
+
* `onSave(text)` persists and returns an error string (or null on success).
|
|
56
|
+
* Resolves when the user closes the editor (Esc/Ctrl+Q). */
|
|
57
|
+
showEditor(title: string, initialText: string, onSave: (text: string) => Promise<string | null>): Promise<void>;
|
|
46
58
|
/**
|
|
47
59
|
* Suspend the blessed screen, run `fn` on the REAL terminal (so command
|
|
48
60
|
* handlers' console.log / ora spinners / @inquirer prompts all work and don't
|
package/dist/tui/screen.js
CHANGED
|
@@ -155,6 +155,9 @@ export function createTuiScreen(opts) {
|
|
|
155
155
|
scheduleRender();
|
|
156
156
|
}
|
|
157
157
|
function markCheckpoint() { return outputBuf.length; }
|
|
158
|
+
let pendingFollowups = [];
|
|
159
|
+
function setPendingFollowups(items) { pendingFollowups = Array.isArray(items) ? items.slice(0, 9) : []; }
|
|
160
|
+
function getPendingFollowups() { return pendingFollowups; }
|
|
158
161
|
function replaceOutputFrom(offset, text) {
|
|
159
162
|
if (offset < 0 || offset > outputBuf.length)
|
|
160
163
|
return;
|
|
@@ -199,6 +202,18 @@ export function createTuiScreen(opts) {
|
|
|
199
202
|
for (const l of t.lines)
|
|
200
203
|
out.push(l);
|
|
201
204
|
});
|
|
205
|
+
// Belt-and-suspenders against blessed "character bleed": when the trace
|
|
206
|
+
// SHRINKS (a long turn collapses into a short new one) blessed's wide-char
|
|
207
|
+
// (emoji) miscount in wrapped lines leaves stale glyphs in the now-uncovered
|
|
208
|
+
// rows. clearRegion alone proved unreliable for this, so when the content is
|
|
209
|
+
// shorter than the pane, pad with blank lines so setContent paints over every
|
|
210
|
+
// visible row. Skip when the user has scrolled up (don't perturb their view)
|
|
211
|
+
// or when content already fills/overflows the pane (no uncovered rows).
|
|
212
|
+
if (!userPinnedTrace) {
|
|
213
|
+
const innerH = (typeof trace.height === 'number' ? trace.height : 0) - 2;
|
|
214
|
+
while (innerH > out.length)
|
|
215
|
+
out.push('');
|
|
216
|
+
}
|
|
202
217
|
const base = trace.childBase || 0;
|
|
203
218
|
try {
|
|
204
219
|
trace.setContent(out.join('\n'));
|
|
@@ -726,11 +741,321 @@ export function createTuiScreen(opts) {
|
|
|
726
741
|
screen.render();
|
|
727
742
|
});
|
|
728
743
|
}
|
|
744
|
+
// ── Document viewer overlay (full-screen scrollable box) ──────
|
|
745
|
+
// Reuses the _pickerOpen guard so the main keypress handler stands down while
|
|
746
|
+
// the viewer owns the keyboard. Content is pre-rendered (markdown/code/text +
|
|
747
|
+
// chalk ANSI); blessed parses the SGR colour codes itself. Images do NOT come
|
|
748
|
+
// through here — they render via runDetached so 24-bit colour isn't downsampled.
|
|
749
|
+
function showViewer(title, lines) {
|
|
750
|
+
return new Promise((resolve) => {
|
|
751
|
+
_pickerOpen = true;
|
|
752
|
+
const box = blessed.box({
|
|
753
|
+
parent: screen, label: ` ${title} — ↑↓/PgUp/PgDn scroll · g/G top/bottom · q/Esc close `,
|
|
754
|
+
border: 'line', top: 0, left: 0, width: '100%', height: '100%',
|
|
755
|
+
tags: false, scrollable: true, alwaysScroll: true, keys: true, mouse: true,
|
|
756
|
+
scrollbar: { ch: ' ', inverse: true }, wrap: true,
|
|
757
|
+
style: { border: { fg: 'cyan' }, label: { fg: 'cyan' } },
|
|
758
|
+
});
|
|
759
|
+
box.setContent(lines.join('\n'));
|
|
760
|
+
function close() {
|
|
761
|
+
_pickerOpen = false;
|
|
762
|
+
try {
|
|
763
|
+
box.destroy();
|
|
764
|
+
}
|
|
765
|
+
catch { /* */ }
|
|
766
|
+
screen.render();
|
|
767
|
+
focusInput();
|
|
768
|
+
resolve();
|
|
769
|
+
}
|
|
770
|
+
const vpage = () => Math.max(1, box.height - 3);
|
|
771
|
+
box.key(['q', 'escape', 'C-c'], () => close());
|
|
772
|
+
box.key(['up', 'k'], () => { box.scroll(-1); screen.render(); });
|
|
773
|
+
box.key(['down', 'j'], () => { box.scroll(1); screen.render(); });
|
|
774
|
+
box.key(['pageup'], () => { box.scroll(-vpage()); screen.render(); });
|
|
775
|
+
box.key(['pagedown', 'space'], () => { box.scroll(vpage()); screen.render(); });
|
|
776
|
+
box.key(['g', 'home'], () => { box.scrollTo(0); screen.render(); });
|
|
777
|
+
box.key(['G', 'end'], () => { box.setScrollPerc(100); screen.render(); });
|
|
778
|
+
box.on('wheelup', () => { box.scroll(-3); screen.render(); });
|
|
779
|
+
box.on('wheeldown', () => { box.scroll(3); screen.render(); });
|
|
780
|
+
box.focus();
|
|
781
|
+
screen.render();
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
// ── Document editor overlay (notepad/vim-lite, manual keypresses) ─────
|
|
785
|
+
// A real multi-line editor. We drive it from the PROGRAM keypress stream (same
|
|
786
|
+
// reason as the main input box: blessed's textarea double-delivers keys on this
|
|
787
|
+
// terminal). Lines are stored as code-point arrays so emoji/CJK caret maths is
|
|
788
|
+
// correct. While open, _pickerOpen suppresses the main onKey handler and a
|
|
789
|
+
// dedicated keypress listener owns the keyboard until the user quits.
|
|
790
|
+
function showEditor(title, initialText, onSave) {
|
|
791
|
+
return new Promise((resolve) => {
|
|
792
|
+
_pickerOpen = true;
|
|
793
|
+
const lines = (initialText.length ? initialText.replace(/\r\n/g, '\n').split('\n') : [''])
|
|
794
|
+
.map((l) => Array.from(l));
|
|
795
|
+
let cr = 0; // cursor row
|
|
796
|
+
let cc = 0; // cursor col (code-point index)
|
|
797
|
+
let top = 0; // first visible row (vertical scroll)
|
|
798
|
+
let dirty = false;
|
|
799
|
+
let msg = ''; // transient status message
|
|
800
|
+
let confirmingQuit = false;
|
|
801
|
+
const box = blessed.box({
|
|
802
|
+
parent: screen, border: 'line', top: 0, left: 0, width: '100%', height: '100%',
|
|
803
|
+
tags: false, style: { border: { fg: 'yellow' }, label: { fg: 'yellow' } },
|
|
804
|
+
});
|
|
805
|
+
const innerH = () => Math.max(1, box.height - 2);
|
|
806
|
+
const innerW = () => Math.max(20, box.width - 2);
|
|
807
|
+
const gutterW = () => String(lines.length).length;
|
|
808
|
+
function setLabel() {
|
|
809
|
+
const flag = dirty ? chalk.red(' ●') : '';
|
|
810
|
+
const pos = `${cr + 1}:${cc + 1}`;
|
|
811
|
+
const hint = confirmingQuit
|
|
812
|
+
? chalk.red('unsaved — ^Q again to discard, ^S to save')
|
|
813
|
+
: chalk.dim('^S save · ^Q/Esc quit · arrows move · Enter newline');
|
|
814
|
+
const note = msg ? ' ' + chalk.green(msg) : '';
|
|
815
|
+
try {
|
|
816
|
+
box.setLabel(` ${title}${flag} ${chalk.dim(pos)} ${hint}${note} `);
|
|
817
|
+
}
|
|
818
|
+
catch { /* */ }
|
|
819
|
+
}
|
|
820
|
+
function clampView() {
|
|
821
|
+
if (cr < top)
|
|
822
|
+
top = cr;
|
|
823
|
+
if (cr >= top + innerH())
|
|
824
|
+
top = cr - innerH() + 1;
|
|
825
|
+
if (top < 0)
|
|
826
|
+
top = 0;
|
|
827
|
+
}
|
|
828
|
+
function render() {
|
|
829
|
+
clampView();
|
|
830
|
+
const g = gutterW();
|
|
831
|
+
const avail = innerW() - (g + 3); // gutter "NNN │ "
|
|
832
|
+
const out = [];
|
|
833
|
+
for (let r = top; r < Math.min(lines.length, top + innerH()); r++) {
|
|
834
|
+
const isCur = r === cr;
|
|
835
|
+
const cps = lines[r];
|
|
836
|
+
// Horizontal window so the caret stays visible on long lines.
|
|
837
|
+
let start = 0;
|
|
838
|
+
if (isCur && cc > avail - 1)
|
|
839
|
+
start = cc - (avail - 1);
|
|
840
|
+
const slice = cps.slice(start, start + avail);
|
|
841
|
+
const gutter = chalk.dim(String(r + 1).padStart(g, ' ') + ' │ ');
|
|
842
|
+
if (isCur) {
|
|
843
|
+
const rel = cc - start;
|
|
844
|
+
const before = slice.slice(0, rel).join('');
|
|
845
|
+
const atCh = slice[rel] ?? ' ';
|
|
846
|
+
const after = slice.slice(rel + 1).join('');
|
|
847
|
+
const lead = start > 0 ? chalk.dim('‹') : '';
|
|
848
|
+
out.push(gutter + lead + before + chalk.inverse(atCh) + after);
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
out.push(gutter + slice.join(''));
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
try {
|
|
855
|
+
box.setContent(out.join('\n'));
|
|
856
|
+
}
|
|
857
|
+
catch { /* */ }
|
|
858
|
+
setLabel();
|
|
859
|
+
scheduleRender();
|
|
860
|
+
}
|
|
861
|
+
function curLine() { return lines[cr]; }
|
|
862
|
+
async function doSave() {
|
|
863
|
+
const text = lines.map((l) => l.join('')).join('\n');
|
|
864
|
+
const err = await onSave(text);
|
|
865
|
+
if (err) {
|
|
866
|
+
msg = '';
|
|
867
|
+
setLabel();
|
|
868
|
+
try {
|
|
869
|
+
box.setLabel(` ${title} ${chalk.red('save failed: ' + err)} `);
|
|
870
|
+
}
|
|
871
|
+
catch { /* */ }
|
|
872
|
+
scheduleRender();
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
dirty = false;
|
|
876
|
+
confirmingQuit = false;
|
|
877
|
+
msg = 'saved';
|
|
878
|
+
render();
|
|
879
|
+
setTimeout(() => { msg = ''; setLabel(); scheduleRender(); }, 1500);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function close() {
|
|
883
|
+
screen.program.removeListener('keypress', editorKey);
|
|
884
|
+
_pickerOpen = false;
|
|
885
|
+
try {
|
|
886
|
+
box.destroy();
|
|
887
|
+
}
|
|
888
|
+
catch { /* */ }
|
|
889
|
+
screen.render();
|
|
890
|
+
focusInput();
|
|
891
|
+
resolve();
|
|
892
|
+
}
|
|
893
|
+
function editorKey(ch, key) {
|
|
894
|
+
const name = (key && key.name) || '';
|
|
895
|
+
const ctrl = !!(key && key.ctrl);
|
|
896
|
+
// Save / quit.
|
|
897
|
+
if (ctrl && name === 's') {
|
|
898
|
+
void doSave();
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if ((ctrl && name === 'q') || name === 'escape') {
|
|
902
|
+
if (dirty && !confirmingQuit) {
|
|
903
|
+
confirmingQuit = true;
|
|
904
|
+
setLabel();
|
|
905
|
+
scheduleRender();
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
close();
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
confirmingQuit = false;
|
|
912
|
+
// Navigation.
|
|
913
|
+
if (name === 'up') {
|
|
914
|
+
if (cr > 0) {
|
|
915
|
+
cr--;
|
|
916
|
+
cc = Math.min(cc, curLine().length);
|
|
917
|
+
}
|
|
918
|
+
render();
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (name === 'down') {
|
|
922
|
+
if (cr < lines.length - 1) {
|
|
923
|
+
cr++;
|
|
924
|
+
cc = Math.min(cc, curLine().length);
|
|
925
|
+
}
|
|
926
|
+
render();
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (name === 'left') {
|
|
930
|
+
if (cc > 0)
|
|
931
|
+
cc--;
|
|
932
|
+
else if (cr > 0) {
|
|
933
|
+
cr--;
|
|
934
|
+
cc = curLine().length;
|
|
935
|
+
}
|
|
936
|
+
render();
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (name === 'right') {
|
|
940
|
+
if (cc < curLine().length)
|
|
941
|
+
cc++;
|
|
942
|
+
else if (cr < lines.length - 1) {
|
|
943
|
+
cr++;
|
|
944
|
+
cc = 0;
|
|
945
|
+
}
|
|
946
|
+
render();
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
if (name === 'home') {
|
|
950
|
+
cc = 0;
|
|
951
|
+
render();
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
if (name === 'end') {
|
|
955
|
+
cc = curLine().length;
|
|
956
|
+
render();
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (name === 'pageup') {
|
|
960
|
+
cr = Math.max(0, cr - innerH());
|
|
961
|
+
cc = Math.min(cc, curLine().length);
|
|
962
|
+
render();
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (name === 'pagedown') {
|
|
966
|
+
cr = Math.min(lines.length - 1, cr + innerH());
|
|
967
|
+
cc = Math.min(cc, curLine().length);
|
|
968
|
+
render();
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
// Editing.
|
|
972
|
+
if (name === 'enter' || name === 'return' || name === 'linefeed') {
|
|
973
|
+
const cur = curLine();
|
|
974
|
+
const tail = cur.slice(cc);
|
|
975
|
+
lines[cr] = cur.slice(0, cc);
|
|
976
|
+
lines.splice(cr + 1, 0, tail);
|
|
977
|
+
cr++;
|
|
978
|
+
cc = 0;
|
|
979
|
+
dirty = true;
|
|
980
|
+
render();
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (name === 'backspace') {
|
|
984
|
+
if (cc > 0) {
|
|
985
|
+
curLine().splice(cc - 1, 1);
|
|
986
|
+
cc--;
|
|
987
|
+
dirty = true;
|
|
988
|
+
}
|
|
989
|
+
else if (cr > 0) {
|
|
990
|
+
const cur = lines.splice(cr, 1)[0];
|
|
991
|
+
cc = lines[cr - 1].length;
|
|
992
|
+
lines[cr - 1] = lines[cr - 1].concat(cur);
|
|
993
|
+
cr--;
|
|
994
|
+
dirty = true;
|
|
995
|
+
}
|
|
996
|
+
render();
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (name === 'delete') {
|
|
1000
|
+
if (cc < curLine().length) {
|
|
1001
|
+
curLine().splice(cc, 1);
|
|
1002
|
+
dirty = true;
|
|
1003
|
+
}
|
|
1004
|
+
else if (cr < lines.length - 1) {
|
|
1005
|
+
const next = lines.splice(cr + 1, 1)[0];
|
|
1006
|
+
lines[cr] = curLine().concat(next);
|
|
1007
|
+
dirty = true;
|
|
1008
|
+
}
|
|
1009
|
+
render();
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
if (name === 'tab') {
|
|
1013
|
+
curLine().splice(cc, 0, ' ', ' ');
|
|
1014
|
+
cc += 2;
|
|
1015
|
+
dirty = true;
|
|
1016
|
+
render();
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
// Printable insert (paste may deliver several chars; respects newlines).
|
|
1020
|
+
if (ch && typeof ch === 'string' && ch.length >= 1 && ch >= ' ' && !(key && (key.ctrl || key.meta))) {
|
|
1021
|
+
for (const c of Array.from(ch)) {
|
|
1022
|
+
curLine().splice(cc, 0, c);
|
|
1023
|
+
cc++;
|
|
1024
|
+
}
|
|
1025
|
+
dirty = true;
|
|
1026
|
+
render();
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
// Multi-line paste arriving as a chunk with embedded newlines.
|
|
1030
|
+
if (ch && typeof ch === 'string' && ch.includes('\n')) {
|
|
1031
|
+
for (const c of Array.from(ch)) {
|
|
1032
|
+
if (c === '\n') {
|
|
1033
|
+
const tail = curLine().slice(cc);
|
|
1034
|
+
lines[cr] = curLine().slice(0, cc);
|
|
1035
|
+
lines.splice(cr + 1, 0, tail);
|
|
1036
|
+
cr++;
|
|
1037
|
+
cc = 0;
|
|
1038
|
+
}
|
|
1039
|
+
else if (c >= ' ') {
|
|
1040
|
+
curLine().splice(cc, 0, c);
|
|
1041
|
+
cc++;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
dirty = true;
|
|
1045
|
+
render();
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
screen.program.on('keypress', editorKey);
|
|
1050
|
+
render();
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
729
1053
|
renderInput();
|
|
730
1054
|
screen.render();
|
|
731
1055
|
return {
|
|
732
1056
|
screen, input, appendOutput, outputLine, markCheckpoint, replaceOutputFrom, prependOutput,
|
|
733
1057
|
traceLine, startTraceTurn, finishTraceTurn, setStatus, setTracePanel, setOutputLabel, clearPanes, toggleTrace, render, focusInput, destroy,
|
|
734
|
-
|
|
1058
|
+
setPendingFollowups, getPendingFollowups,
|
|
1059
|
+
pickerOpen, showPicker, showViewer, showEditor, runDetached,
|
|
735
1060
|
};
|
|
736
1061
|
}
|