feat: Phase2 JLine终端交互增强 - 行编辑、历史、Tab补全、信号处理

JLine 3 集成:
- ReplSession重写: JLine Terminal + LineReader 替代Scanner
- 支持行编辑(光标移动、删除、Home/End等)
- 持久化历史记录(~/.claude-code-java/history)
- 上下箭头浏览输入历史
- Ctrl+C取消当前输入、Ctrl+D退出
- JLine失败时自动降级到Scanner模式

Tab补全:
- ClaudeCodeCompleter: 斜杠命令补全(/后按Tab)
- 支持命令别名补全(如 /q → exit)
- 分组显示(Commands / Aliases)

新增命令:
- /model: 显示当前AI模型信息
- /compact: 压缩对话上下文
- /cost: Token用量显示(占位)

改进:
- HelpCommand清理为统一格式
- Banner增加终端信息显示和快捷键提示
- 彩色提示符使用AttributedStringBuilder

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
liuzh
2026-04-01 20:18:59 +08:00
co-authored by Copilot
parent 70c9ebed2b
commit b29d61581a
7 changed files with 358 additions and 53 deletions
@@ -5,17 +5,32 @@ import com.claudecode.command.CommandRegistry;
import com.claudecode.console.*;
import com.claudecode.core.AgentLoop;
import com.claudecode.tool.ToolRegistry;
import org.jline.reader.*;
import org.jline.reader.impl.DefaultParser;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Scanner;
/**
* REPL 会话管理器 —— 对应 claude-code/src/REPL.tsx。
* <p>
* 管理用户输入循环、命令分发、Agent 调用和输出渲染。
* 当前版本使用 Scanner 作为输入方式(Phase 2 会升级到 JLine)。
* 使用 JLine 3 提供丰富的终端交互体验:
* <ul>
* <li>行编辑(光标移动、删除、粘贴)</li>
* <li>历史记录(上下箭头浏览、持久化到文件)</li>
* <li>Tab 补全(斜杠命令、工具名称)</li>
* <li>信号处理(Ctrl+C 取消当前输入、Ctrl+D 退出)</li>
* </ul>
* 当 JLine 初始化失败时自动降级到 Scanner 模式。
*/
public class ReplSession {
@@ -42,16 +57,18 @@ public class ReplSession {
this.markdownRenderer = new MarkdownRenderer(out);
this.spinner = new SpinnerAnimation(out);
// 注册 AgentLoop 事件回调
setupAgentCallbacks();
}
/** 注册 AgentLoop 事件回调,驱动控制台 UI 渲染 */
private void setupAgentCallbacks() {
agentLoop.setOnToolEvent(event -> {
switch (event.phase()) {
case START -> {
spinner.stop();
toolStatusRenderer.renderStart(event.toolName(), event.arguments());
}
case END -> {
toolStatusRenderer.renderEnd(event.toolName(), event.result());
}
case END -> toolStatusRenderer.renderEnd(event.toolName(), event.result());
}
});
@@ -61,64 +78,151 @@ public class ReplSession {
}
/**
* 启动 REPL 主循环
* 启动 REPL —— 优先使用 JLine,失败时降级到 Scanner
*/
public void start() {
try {
startWithJLine();
} catch (Exception e) {
log.warn("JLine 初始化失败,降级到 Scanner 模式: {}", e.getMessage());
startWithScanner();
}
}
// ==================== JLine 模式 ====================
private void startWithJLine() throws IOException {
Path historyDir = Path.of(System.getProperty("user.home"), ".claude-code-java");
Files.createDirectories(historyDir);
try (Terminal terminal = TerminalBuilder.builder()
.system(true)
.build()) {
LineReader reader = LineReaderBuilder.builder()
.terminal(terminal)
.parser(new DefaultParser())
.completer(new ClaudeCodeCompleter(commandRegistry, toolRegistry))
.variable(LineReader.HISTORY_FILE, historyDir.resolve("history"))
.variable(LineReader.HISTORY_SIZE, 1000)
.option(LineReader.Option.CASE_INSENSITIVE, true)
.option(LineReader.Option.AUTO_LIST, true)
.build();
// 构建彩色提示符
String prompt = new AttributedStringBuilder()
.style(AttributedStyle.BOLD.foreground(AttributedStyle.CYAN))
.append(" ")
.style(AttributedStyle.DEFAULT)
.toAnsi(terminal);
// 续行提示符(多行输入时显示)
String rightPrompt = new AttributedStringBuilder()
.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.BRIGHT))
.append("")
.toAnsi(terminal);
printBanner(terminal);
CommandContext cmdContext = new CommandContext(agentLoop, toolRegistry, out, () -> running = false);
while (running) {
String input;
try {
input = reader.readLine(prompt).strip();
} catch (UserInterruptException e) {
// Ctrl+C —— 取消当前输入,继续等待
spinner.stop();
out.println(AnsiStyle.dim(" ^C"));
continue;
} catch (EndOfFileException e) {
// Ctrl+D —— 退出
break;
}
if (input.isEmpty()) {
continue;
}
handleInput(input, cmdContext);
}
out.println(AnsiStyle.dim("\n Goodbye! 👋\n"));
}
}
/** 打印启动 BannerJLine 模式) */
private void printBanner(Terminal terminal) {
BannerPrinter.printCompact(out);
out.println(AnsiStyle.dim(" Working directory: " + System.getProperty("user.dir")));
out.println(AnsiStyle.dim(" Tools: " + toolRegistry.size() + " registered"));
out.println(AnsiStyle.dim(" Terminal: " + terminal.getType()
+ " (" + terminal.getWidth() + "×" + terminal.getHeight() + ")"));
out.println(AnsiStyle.dim(" Tip: Tab to complete commands, ↑↓ to browse history, Ctrl+D to exit"));
out.println();
}
// ==================== Scanner 降级模式 ====================
private void startWithScanner() {
BannerPrinter.printCompact(out);
out.println(AnsiStyle.dim(" Working directory: " + System.getProperty("user.dir")));
out.println(AnsiStyle.dim(" Tools: " + toolRegistry.size() + " registered"));
out.println(AnsiStyle.dim(" Mode: Scanner (basic input)"));
out.println();
Scanner scanner = new Scanner(System.in);
CommandContext cmdContext = new CommandContext(agentLoop, toolRegistry, out, () -> running = false);
while (running) {
// 输入提示符
out.print(AnsiStyle.BOLD + AnsiStyle.BRIGHT_CYAN + " " + AnsiStyle.RESET);
out.print(AnsiStyle.BOLD + AnsiStyle.BRIGHT_CYAN + " " + AnsiStyle.RESET);
out.flush();
String input;
try {
if (!scanner.hasNextLine()) {
break; // EOF (Ctrl+D)
}
if (!scanner.hasNextLine()) break;
input = scanner.nextLine().strip();
} catch (Exception e) {
break;
}
if (input.isEmpty()) {
continue;
}
if (input.isEmpty()) continue;
// 检查斜杠命令
if (commandRegistry.isCommand(input)) {
var result = commandRegistry.dispatch(input, cmdContext);
result.ifPresent(out::println);
out.println();
continue;
}
// 调用 Agent 循环
try {
spinner.start("Thinking...");
String response = agentLoop.run(input);
spinner.stop();
out.println();
markdownRenderer.render(response);
out.println();
} catch (Exception e) {
spinner.stop();
out.println(AnsiStyle.red("\n ✗ Error: " + e.getMessage()));
log.error("Agent 循环异常", e);
out.println();
}
handleInput(input, cmdContext);
}
out.println(AnsiStyle.dim("\n Goodbye! 👋\n"));
}
// ==================== 公共输入处理 ====================
/** 处理用户输入(命令分发或 Agent 调用) */
private void handleInput(String input, CommandContext cmdContext) {
// 斜杠命令
if (commandRegistry.isCommand(input)) {
var result = commandRegistry.dispatch(input, cmdContext);
result.ifPresent(out::println);
out.println();
return;
}
// Agent 循环
try {
spinner.start("Thinking...");
String response = agentLoop.run(input);
spinner.stop();
out.println();
markdownRenderer.render(response);
out.println();
} catch (Exception e) {
spinner.stop();
out.println(AnsiStyle.red("\n ✗ Error: " + e.getMessage()));
log.error("Agent 循环异常", e);
out.println();
}
}
public void stop() {
running = false;
}