ikie-cli 0.1.14 → 0.1.15
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/repl.js +150 -14
- package/dist/theme.d.ts +7 -0
- package/dist/theme.js +27 -0
- package/package.json +1 -1
package/dist/repl.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as readline from 'node:readline';
|
|
2
2
|
import { execSync, exec } from 'child_process';
|
|
3
3
|
import { restoreStdinListeners } from './agent.js';
|
|
4
|
-
import { c, PROMPT, CONTINUE_PROMPT, PROMPT_ARROW, printPromptHeader, modeTag, drawBanner, infoLine, successLine, errorLine, THEMES, setTheme, stripAnsi, contextRing, } from './theme.js';
|
|
4
|
+
import { c, PROMPT, CONTINUE_PROMPT, PROMPT_ARROW, printPromptHeader, modeTag, drawBanner, infoLine, successLine, errorLine, THEMES, setTheme, stripAnsi, contextRing, renderSlashMenu, } from './theme.js';
|
|
5
5
|
import { renderMarkdown } from './renderer.js';
|
|
6
6
|
import { loadAllMemory } from './memory.js';
|
|
7
7
|
import { HOME_DIR, saveConfig, DEFAULT_MODEL, IKIE_HOST, IKIE_API_BASE, isLoggedIn, getApiKey } from './config.js';
|
|
@@ -102,22 +102,36 @@ const SLASH_CMDS = [
|
|
|
102
102
|
{ name: 'logout', desc: 'Sign out of ikie account' },
|
|
103
103
|
{ name: 'exit', desc: 'Exit Ikie' },
|
|
104
104
|
];
|
|
105
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Compute live slash-menu items for the current input line.
|
|
107
|
+
* Returns rich items (name/desc/insert) plus the typed fragment to highlight.
|
|
108
|
+
*/
|
|
109
|
+
function computeSlashMenu(line) {
|
|
106
110
|
if (!line.startsWith('/'))
|
|
107
|
-
return [
|
|
108
|
-
const
|
|
111
|
+
return { items: [], fragment: '' };
|
|
112
|
+
const afterSlash = line.slice(1);
|
|
113
|
+
const parts = afterSlash.split(/\s+/);
|
|
109
114
|
const cmd = parts[0]?.toLowerCase() ?? '';
|
|
115
|
+
// Sub-command stage: "/cmd <frag>" where cmd has subs.
|
|
110
116
|
if (parts.length >= 2) {
|
|
111
|
-
const match = SLASH_CMDS.find(
|
|
112
|
-
if (match?.subs)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
const match = SLASH_CMDS.find(s => s.name === cmd);
|
|
118
|
+
if (!match?.subs)
|
|
119
|
+
return { items: [], fragment: '' };
|
|
120
|
+
const frag = parts[1].toLowerCase();
|
|
121
|
+
const items = match.subs
|
|
122
|
+
.filter(s => s.name.startsWith(frag))
|
|
123
|
+
.map(s => ({ name: `${cmd} ${s.name}`, desc: s.desc, insert: `/${cmd} ${s.name} ` }));
|
|
124
|
+
return { items, fragment: frag };
|
|
118
125
|
}
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
// Command-name stage: "/<frag>".
|
|
127
|
+
const items = SLASH_CMDS
|
|
128
|
+
.filter(s => s.name.startsWith(cmd))
|
|
129
|
+
.map(s => ({
|
|
130
|
+
name: s.name,
|
|
131
|
+
desc: s.desc,
|
|
132
|
+
insert: `/${s.name}${s.subs || s.args ? ' ' : ''}`,
|
|
133
|
+
}));
|
|
134
|
+
return { items, fragment: cmd };
|
|
121
135
|
}
|
|
122
136
|
function printHelp() {
|
|
123
137
|
console.log(`
|
|
@@ -847,7 +861,6 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
847
861
|
terminal: true,
|
|
848
862
|
historySize: MAX_HISTORY,
|
|
849
863
|
prompt: PROMPT,
|
|
850
|
-
completer: slashCompleter,
|
|
851
864
|
});
|
|
852
865
|
let multilineBuffer = '';
|
|
853
866
|
let busy = false;
|
|
@@ -860,6 +873,77 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
860
873
|
let pasteMode = false;
|
|
861
874
|
let pasteBuffer = '';
|
|
862
875
|
const pastedBlocks = new Map();
|
|
876
|
+
// ── Live slash-command menu state ──────────────────────────────────────────
|
|
877
|
+
let currentPrompt = PROMPT; // the prompt string readline is showing
|
|
878
|
+
let menuOpen = false;
|
|
879
|
+
let menuItems = [];
|
|
880
|
+
let menuFragment = '';
|
|
881
|
+
let menuSel = 0;
|
|
882
|
+
let menuRows = 0; // menu lines currently drawn below input
|
|
883
|
+
const inputCol = () => stripAnsi(currentPrompt).length + (rl.cursor ?? 0);
|
|
884
|
+
const eraseMenu = () => {
|
|
885
|
+
if (menuRows === 0)
|
|
886
|
+
return;
|
|
887
|
+
let out = '\r';
|
|
888
|
+
for (let i = 0; i < menuRows; i++)
|
|
889
|
+
out += '\n\x1b[2K';
|
|
890
|
+
out += `\x1b[${menuRows}A\r`;
|
|
891
|
+
const col = inputCol();
|
|
892
|
+
if (col > 0)
|
|
893
|
+
out += `\x1b[${col}C`;
|
|
894
|
+
process.stdout.write(out);
|
|
895
|
+
menuRows = 0;
|
|
896
|
+
};
|
|
897
|
+
const drawMenu = () => {
|
|
898
|
+
eraseMenu();
|
|
899
|
+
if (!menuOpen || menuItems.length === 0)
|
|
900
|
+
return;
|
|
901
|
+
const cols = Math.max(40, process.stdout.columns ?? 80);
|
|
902
|
+
const rows = renderSlashMenu(menuItems, menuSel, menuFragment, cols);
|
|
903
|
+
let out = '\r';
|
|
904
|
+
for (const r of rows)
|
|
905
|
+
out += `\n\x1b[2K${r}`;
|
|
906
|
+
out += `\x1b[${rows.length}A\r`;
|
|
907
|
+
const col = inputCol();
|
|
908
|
+
if (col > 0)
|
|
909
|
+
out += `\x1b[${col}C`;
|
|
910
|
+
process.stdout.write(out);
|
|
911
|
+
menuRows = rows.length;
|
|
912
|
+
};
|
|
913
|
+
const closeMenu = () => {
|
|
914
|
+
if (!menuOpen && menuRows === 0)
|
|
915
|
+
return;
|
|
916
|
+
menuOpen = false;
|
|
917
|
+
eraseMenu();
|
|
918
|
+
};
|
|
919
|
+
const updateMenu = () => {
|
|
920
|
+
const line = rl.line ?? '';
|
|
921
|
+
const { items, fragment } = computeSlashMenu(line);
|
|
922
|
+
// Hide when the sole match is exactly what's typed (nothing left to suggest).
|
|
923
|
+
const exhausted = items.length === 1 && line === `/${items[0].name}`;
|
|
924
|
+
if (items.length && line.startsWith('/') && !exhausted) {
|
|
925
|
+
menuItems = items;
|
|
926
|
+
menuFragment = fragment;
|
|
927
|
+
menuSel = 0;
|
|
928
|
+
menuOpen = true;
|
|
929
|
+
drawMenu();
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
closeMenu();
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
const acceptSelection = () => {
|
|
936
|
+
if (!menuOpen || !menuItems[menuSel])
|
|
937
|
+
return;
|
|
938
|
+
const insert = menuItems[menuSel].insert;
|
|
939
|
+
eraseMenu();
|
|
940
|
+
menuOpen = false;
|
|
941
|
+
// Replace the line via public readline API (cursor→end, kill line, write).
|
|
942
|
+
rl.write(null, { ctrl: true, name: 'e' });
|
|
943
|
+
rl.write(null, { ctrl: true, name: 'u' });
|
|
944
|
+
rl.write(insert);
|
|
945
|
+
updateMenu(); // a trailing space may surface a sub-command menu
|
|
946
|
+
};
|
|
863
947
|
const readlineDataListeners = process.stdin.rawListeners('data').slice();
|
|
864
948
|
for (const fn of readlineDataListeners) {
|
|
865
949
|
process.stdin.removeListener('data', fn);
|
|
@@ -886,8 +970,52 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
886
970
|
};
|
|
887
971
|
const routeInputData = (data) => {
|
|
888
972
|
const text = data.toString('utf8');
|
|
973
|
+
// ── Live slash-menu navigation (only while the menu is open) ──
|
|
974
|
+
if (menuOpen) {
|
|
975
|
+
if (text === '\x1b[A') {
|
|
976
|
+
menuSel = (menuSel - 1 + menuItems.length) % menuItems.length;
|
|
977
|
+
drawMenu();
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if (text === '\x1b[B') {
|
|
981
|
+
menuSel = (menuSel + 1) % menuItems.length;
|
|
982
|
+
drawMenu();
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (text === '\t') {
|
|
986
|
+
acceptSelection();
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (text === '\x1b') {
|
|
990
|
+
closeMenu();
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (text === '\r' || text === '\n') {
|
|
994
|
+
const item = menuItems[menuSel];
|
|
995
|
+
if (item) {
|
|
996
|
+
// Fill the highlighted command. If it takes no args/subs (no trailing
|
|
997
|
+
// space) submit it immediately; otherwise keep editing for the arg.
|
|
998
|
+
const submit = !item.insert.endsWith(' ');
|
|
999
|
+
eraseMenu();
|
|
1000
|
+
menuOpen = false;
|
|
1001
|
+
rl.write(null, { ctrl: true, name: 'e' });
|
|
1002
|
+
rl.write(null, { ctrl: true, name: 'u' });
|
|
1003
|
+
rl.write(item.insert);
|
|
1004
|
+
if (submit)
|
|
1005
|
+
forwardToReadline(Buffer.from('\r'));
|
|
1006
|
+
else
|
|
1007
|
+
updateMenu();
|
|
1008
|
+
}
|
|
1009
|
+
else {
|
|
1010
|
+
closeMenu();
|
|
1011
|
+
forwardToReadline(data);
|
|
1012
|
+
}
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
889
1016
|
// Shift+Tab (CSI Z) → cycle agent⇄plan mode live at the prompt.
|
|
890
1017
|
if (text === '\x1b[Z') {
|
|
1018
|
+
closeMenu();
|
|
891
1019
|
const next = agent.getMode() === 'agent' ? 'plan' : 'agent';
|
|
892
1020
|
agent.setMode(next);
|
|
893
1021
|
const saved = rl.line || '';
|
|
@@ -900,6 +1028,7 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
900
1028
|
}
|
|
901
1029
|
// Check for Ctrl+V (0x16) - try to paste image from clipboard
|
|
902
1030
|
if (data.length === 1 && data[0] === 0x16) {
|
|
1031
|
+
closeMenu();
|
|
903
1032
|
// Check if clipboard has an image
|
|
904
1033
|
if (hasClipboardImage()) {
|
|
905
1034
|
try {
|
|
@@ -923,6 +1052,7 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
923
1052
|
return;
|
|
924
1053
|
}
|
|
925
1054
|
if (!pasteMode && !text.includes('\x1b[200~') && /[\r\n]/.test(text.trim()) && text.length > 3) {
|
|
1055
|
+
closeMenu();
|
|
926
1056
|
const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
927
1057
|
rl.write(normalized);
|
|
928
1058
|
return;
|
|
@@ -946,11 +1076,13 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
946
1076
|
const start = rest.indexOf('\x1b[200~');
|
|
947
1077
|
if (start === -1) {
|
|
948
1078
|
forwardToReadline(Buffer.from(rest, 'utf8'));
|
|
1079
|
+
updateMenu();
|
|
949
1080
|
return;
|
|
950
1081
|
}
|
|
951
1082
|
if (start > 0) {
|
|
952
1083
|
forwardToReadline(Buffer.from(rest.slice(0, start), 'utf8'));
|
|
953
1084
|
}
|
|
1085
|
+
closeMenu();
|
|
954
1086
|
pasteMode = true;
|
|
955
1087
|
rest = rest.slice(start + '\x1b[200~'.length);
|
|
956
1088
|
}
|
|
@@ -976,7 +1108,9 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
976
1108
|
return `${expanded}\n\n${attachments.join('\n\n')}`;
|
|
977
1109
|
};
|
|
978
1110
|
const showPrompt = () => {
|
|
1111
|
+
closeMenu();
|
|
979
1112
|
if (multilineBuffer) {
|
|
1113
|
+
currentPrompt = CONTINUE_PROMPT;
|
|
980
1114
|
rl.setPrompt(CONTINUE_PROMPT);
|
|
981
1115
|
}
|
|
982
1116
|
else {
|
|
@@ -991,6 +1125,7 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
991
1125
|
const labels = imageState.pending.map(image => `[Image #${image.id}]`).join(' ');
|
|
992
1126
|
process.stdout.write(`${c.muted(' attached')} ${c.secondary(labels)}\n`);
|
|
993
1127
|
}
|
|
1128
|
+
currentPrompt = prompt;
|
|
994
1129
|
rl.setPrompt(prompt);
|
|
995
1130
|
}
|
|
996
1131
|
rl.prompt();
|
|
@@ -1013,6 +1148,7 @@ export async function startREPL(agent, config, projectContext, oneShot) {
|
|
|
1013
1148
|
showPrompt();
|
|
1014
1149
|
});
|
|
1015
1150
|
const handleLine = async (raw) => {
|
|
1151
|
+
closeMenu();
|
|
1016
1152
|
const line = raw.trim();
|
|
1017
1153
|
if (!line) {
|
|
1018
1154
|
showPrompt();
|
package/dist/theme.d.ts
CHANGED
|
@@ -74,6 +74,13 @@ interface DiffOpts {
|
|
|
74
74
|
export declare function toolDiffBlock(oldStr: string, newStr: string, ms: number, opts?: DiffOpts): string;
|
|
75
75
|
export declare function toolSuccessLine(ms: number, preview?: string): string;
|
|
76
76
|
export declare function toolErrorLine(msg: string): string;
|
|
77
|
+
export interface SlashMenuItem {
|
|
78
|
+
name: string;
|
|
79
|
+
desc: string;
|
|
80
|
+
insert: string;
|
|
81
|
+
}
|
|
82
|
+
/** Render the live slash-command dropdown as an array of fully-styled rows. */
|
|
83
|
+
export declare function renderSlashMenu(items: SlashMenuItem[], sel: number, fragment: string, cols: number): string[];
|
|
77
84
|
export declare function successLine(msg: string): string;
|
|
78
85
|
export declare function errorLine(msg: string): string;
|
|
79
86
|
export declare function warnLine(msg: string): string;
|
package/dist/theme.js
CHANGED
|
@@ -545,6 +545,33 @@ function formatDuration(ms) {
|
|
|
545
545
|
const s = ms / 1000;
|
|
546
546
|
return s < 10 ? `${s.toFixed(1)}s` : `${Math.round(s)}s`;
|
|
547
547
|
}
|
|
548
|
+
/** Render the live slash-command dropdown as an array of fully-styled rows. */
|
|
549
|
+
export function renderSlashMenu(items, sel, fragment, cols) {
|
|
550
|
+
if (!items.length)
|
|
551
|
+
return [];
|
|
552
|
+
const nameW = Math.min(22, Math.max(...items.map(i => i.name.length)));
|
|
553
|
+
const frag = fragment.toLowerCase();
|
|
554
|
+
return items.map((item, i) => {
|
|
555
|
+
const selected = i === sel;
|
|
556
|
+
const bar = selected ? c.primary('▌') : ' ';
|
|
557
|
+
// Highlight the matched prefix within the command name.
|
|
558
|
+
let name;
|
|
559
|
+
if (frag && item.name.toLowerCase().startsWith(frag)) {
|
|
560
|
+
const hit = item.name.slice(0, frag.length);
|
|
561
|
+
const rest = item.name.slice(frag.length);
|
|
562
|
+
name = `${c.accent.bold(hit)}${selected ? c.white.bold(rest) : c.white(rest)}`;
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
name = selected ? c.white.bold(item.name) : c.white(item.name);
|
|
566
|
+
}
|
|
567
|
+
const namePadded = name + ' '.repeat(Math.max(0, nameW - item.name.length));
|
|
568
|
+
// Description, clamped to the remaining width.
|
|
569
|
+
const used = 2 /* bar+space */ + 1 + nameW + 2; // leading "/" + gap
|
|
570
|
+
const descMax = Math.max(8, cols - used - 2);
|
|
571
|
+
const desc = item.desc.length > descMax ? item.desc.slice(0, descMax - 1) + '…' : item.desc;
|
|
572
|
+
return `${bar} ${c.primary('/')}${namePadded} ${c.dim(desc)}`;
|
|
573
|
+
});
|
|
574
|
+
}
|
|
548
575
|
export function successLine(msg) {
|
|
549
576
|
return ` ${c.success('ok')} ${c.muted(msg)}`;
|
|
550
577
|
}
|