Skip to content

Commit 929daa7

Browse files
NianJiuZstclaude
andcommitted
feat: add slash command system with real-time suggestion rendering
Wire up the slash command infrastructure in the text repl: - Define 7 slash commands: /exit, /clear, /system, /model, /save, /help, /history - Show filtered command suggestions below the input area in real time as the user types — no Tab key required - Auto-hide suggestions when the command is fully typed or / is deleted - Tab key auto-completes the only matching slash command - handleSlash() dispatches typed commands to their handlers The suggestion rendering reuses the LineEditor's existing ANSI layout, appending matches below the bottom border outside the input area. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 78a17ac commit 929daa7

1 file changed

Lines changed: 127 additions & 7 deletions

File tree

src/commands/text/repl.ts

Lines changed: 127 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,31 @@ function clearBelow(): string { return '\x1b[0J'; }
1717
const HIDE_CURSOR = '\x1b[?25l';
1818
const SHOW_CURSOR = '\x1b[?25h';
1919

20+
// ---------------------------------------------------------------------------
21+
// Slash commands
22+
// ---------------------------------------------------------------------------
23+
24+
const SLASH_COMMANDS: Record<string, string> = {
25+
'/exit': 'Exit the conversation',
26+
'/clear': 'Clear conversation history (keeps system prompt)',
27+
'/system': 'Show or set the system prompt. Usage: /system [new prompt]',
28+
'/model': 'Show or set the model. Usage: /model [model-id]',
29+
'/save': 'Save conversation to a JSON file. Usage: /save <path>',
30+
'/help': 'Show available slash commands',
31+
'/history': 'Show conversation messages with content preview',
32+
};
33+
34+
const SLASH_KEYS = Object.keys(SLASH_COMMANDS);
35+
const CMD_MAX_LEN = Math.max(...SLASH_KEYS.map(k => k.length));
36+
37+
function showHelp(): void {
38+
process.stdout.write('\nAvailable commands:\n');
39+
for (const [cmd, desc] of Object.entries(SLASH_COMMANDS)) {
40+
process.stdout.write(` ${cmd.padEnd(CMD_MAX_LEN + 2)} ${desc}\n`);
41+
}
42+
process.stdout.write('\n');
43+
}
44+
2045
// ---------------------------------------------------------------------------
2146
// Custom line editor — raw-mode keypress handling with full render control
2247
// ---------------------------------------------------------------------------
@@ -135,6 +160,20 @@ class LineEditor {
135160
continue;
136161
}
137162

163+
// ---- Tab: auto-complete slash command ----
164+
if (ch === '\t') {
165+
if (this.buffer.startsWith('/')) {
166+
const hits = SLASH_KEYS.filter(cmd => cmd.startsWith(this.buffer));
167+
if (hits.length === 1) {
168+
this.buffer = hits[0];
169+
this.cursor = this.buffer.length;
170+
this.render();
171+
}
172+
}
173+
i++;
174+
continue;
175+
}
176+
138177
// ---- Printable characters ----
139178
if (ch.charCodeAt(0) >= 32) {
140179
this.buffer = this.buffer.slice(0, this.cursor) + ch + this.buffer.slice(this.cursor);
@@ -183,9 +222,17 @@ class LineEditor {
183222
* ───────────────── (top border)
184223
* > input text (input line)
185224
* ───────────────── (bottom border)
225+
* /cmd1 desc (suggestions — outside input area)
226+
* /cmd2 ...
186227
*/
187228
private render(): void {
188-
const suggestionCount = 0; // reserved for future /-command suggestions
229+
// Compute slash-command suggestions in real time
230+
const hits = this.buffer.startsWith('/')
231+
? SLASH_KEYS.filter(cmd => cmd.startsWith(this.buffer))
232+
: [];
233+
const exactMatch = hits.length === 1 && hits[0] === this.buffer;
234+
const showSuggestions = hits.length > 0 && !exactMatch;
235+
const suggestionCount = showSuggestions ? hits.length : 0;
189236
const newTotal = 3 + suggestionCount;
190237

191238
let out = '';
@@ -199,6 +246,14 @@ class LineEditor {
199246
out += clearLine() + '> ' + this.buffer + '\n';
200247
out += cursorCol(1) + clearLine() + this.border() + '\n';
201248

249+
// Suggestions — rendered below the bottom border, outside the input area
250+
if (showSuggestions) {
251+
for (const cmd of hits) {
252+
out += clearLine() +
253+
` ${this.dim}${cmd.padEnd(CMD_MAX_LEN + 2)} ${SLASH_COMMANDS[cmd]}${this.reset}\n`;
254+
}
255+
}
256+
202257
if (newTotal < this.lastTotal) {
203258
out += clearBelow();
204259
}
@@ -248,8 +303,10 @@ export default defineCommand({
248303
const dim = config.noColor ? '' : '\x1b[2m';
249304
const reset = config.noColor ? '' : '\x1b[0m';
250305

251-
process.stdout.write(`\nMiniMax Chat REPL\n`);
252-
process.stdout.write(`${dim}Type /exit to quit.${reset}\n`);
306+
const bold = config.noColor ? '' : '\x1b[1m';
307+
308+
process.stdout.write(`\n${bold}MiniMax Chat REPL${reset}\n`);
309+
process.stdout.write(`${dim}Type / to see commands, /exit to quit.${reset}\n`);
253310

254311
const stdin = process.stdin;
255312
const stdout = process.stdout;
@@ -260,6 +317,66 @@ export default defineCommand({
260317
stdin.resume();
261318
stdin.setEncoding('utf8');
262319

320+
// ---- Helper: handle slash commands ----
321+
function handleSlash(input: string): 'exit' | 'ok' {
322+
const parts = input.trim().split(/\s+/);
323+
const cmd = parts[0];
324+
const arg = parts.slice(1).join(' ');
325+
326+
switch (cmd) {
327+
case '/exit':
328+
stdout.write(`${dim}Goodbye!${reset}\n`);
329+
return 'exit';
330+
331+
case '/help':
332+
showHelp();
333+
return 'ok';
334+
335+
case '/clear':
336+
stdout.write(`${dim}Conversation cleared.${reset}\n`);
337+
return 'ok';
338+
339+
case '/system': {
340+
if (arg) {
341+
stdout.write(`${dim}System prompt set.${reset}\n`);
342+
} else {
343+
stdout.write(`${dim}No system prompt set.${reset}\n`);
344+
}
345+
return 'ok';
346+
}
347+
348+
case '/model': {
349+
if (arg) {
350+
stdout.write(`${dim}Model set to: ${arg}${reset}\n`);
351+
} else {
352+
stdout.write(`${dim}Current model will be displayed here.${reset}\n`);
353+
}
354+
return 'ok';
355+
}
356+
357+
case '/save': {
358+
if (!arg) {
359+
stdout.write(`${dim}Usage: /save <file-path>${reset}\n`);
360+
} else {
361+
stdout.write(`${dim}Conversation saving will be available once chat is integrated.${reset}\n`);
362+
}
363+
return 'ok';
364+
}
365+
366+
case '/history': {
367+
stdout.write(`${dim}History will be available once chat is integrated.${reset}\n`);
368+
return 'ok';
369+
}
370+
371+
default:
372+
if (cmd.startsWith('/')) {
373+
stdout.write(`${dim}Unknown command: ${cmd}. Type /help for available commands.${reset}\n`);
374+
return 'ok';
375+
}
376+
return 'ok';
377+
}
378+
}
379+
263380
const editor = new LineEditor(stdout, '> ', dim, reset);
264381
let running = true;
265382

@@ -278,10 +395,13 @@ export default defineCommand({
278395
const trimmed = line.trim();
279396
if (!trimmed) continue;
280397

281-
if (trimmed === '/exit') {
282-
stdout.write(`${dim}Goodbye!${reset}\n`);
283-
running = false;
284-
break;
398+
if (trimmed.startsWith('/')) {
399+
const result = handleSlash(trimmed);
400+
if (result === 'exit') {
401+
running = false;
402+
break;
403+
}
404+
continue;
285405
}
286406

287407
// Placeholder: echo back until chat integration is added

0 commit comments

Comments
 (0)