diff --git a/src/main/java/com/claudecode/command/impl/CompactCommand.java b/src/main/java/com/claudecode/command/impl/CompactCommand.java new file mode 100644 index 0000000..70bfdac --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/CompactCommand.java @@ -0,0 +1,34 @@ +package com.claudecode.command.impl; + +import com.claudecode.command.CommandContext; +import com.claudecode.command.SlashCommand; +import com.claudecode.console.AnsiStyle; + +/** + * /compact 命令 —— 压缩当前对话上下文。 + *
+ * 对应 claude-code/src/commands/compact.ts。 + * 当前为简化实现,直接清空历史并提示用户。 + */ +public class CompactCommand implements SlashCommand { + + @Override + public String name() { + return "compact"; + } + + @Override + public String description() { + return "Compact conversation context"; + } + + @Override + public String execute(String args, CommandContext context) { + if (context.agentLoop() != null) { + int before = context.agentLoop().getMessageHistory().size(); + context.agentLoop().reset(); + return AnsiStyle.green(" ✓ Context compacted: " + before + " messages → 1 (system prompt only)"); + } + return AnsiStyle.yellow(" ⚠ No active conversation to compact."); + } +} diff --git a/src/main/java/com/claudecode/command/impl/CostCommand.java b/src/main/java/com/claudecode/command/impl/CostCommand.java new file mode 100644 index 0000000..e39464a --- /dev/null +++ b/src/main/java/com/claudecode/command/impl/CostCommand.java @@ -0,0 +1,44 @@ +package com.claudecode.command.impl; + +import com.claudecode.command.CommandContext; +import com.claudecode.command.SlashCommand; +import com.claudecode.console.AnsiStyle; + +/** + * /cost 命令 —— 显示 Token 使用量和费用估算。 + *
+ * 对应 claude-code/src/commands/cost.ts。
+ * 当前为占位实现,后续接入实际 Token 统计。
+ */
+public class CostCommand implements SlashCommand {
+
+ @Override
+ public String name() {
+ return "cost";
+ }
+
+ @Override
+ public String description() {
+ return "Show token usage and cost";
+ }
+
+ @Override
+ public String execute(String args, CommandContext context) {
+ int msgCount = 0;
+ if (context.agentLoop() != null) {
+ msgCount = context.agentLoop().getMessageHistory().size();
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("\n");
+ sb.append(AnsiStyle.bold(" Token Usage:\n\n"));
+ sb.append(" Messages: ").append(AnsiStyle.cyan(String.valueOf(msgCount))).append("\n");
+ sb.append(" Input tokens: ").append(AnsiStyle.dim("(tracking not yet implemented)")).append("\n");
+ sb.append(" Output tokens:").append(AnsiStyle.dim("(tracking not yet implemented)")).append("\n");
+ sb.append(" Est. cost: ").append(AnsiStyle.dim("(tracking not yet implemented)")).append("\n");
+ sb.append("\n");
+ sb.append(AnsiStyle.dim(" Token tracking will be added in a future update."));
+
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/com/claudecode/command/impl/HelpCommand.java b/src/main/java/com/claudecode/command/impl/HelpCommand.java
index 2c5d5cd..5793ad0 100644
--- a/src/main/java/com/claudecode/command/impl/HelpCommand.java
+++ b/src/main/java/com/claudecode/command/impl/HelpCommand.java
@@ -1,6 +1,7 @@
package com.claudecode.command.impl;
import com.claudecode.command.CommandContext;
+import com.claudecode.command.CommandRegistry;
import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle;
@@ -31,24 +32,21 @@ public class HelpCommand implements SlashCommand {
StringBuilder sb = new StringBuilder();
sb.append(AnsiStyle.bold("\n Available Commands:\n\n"));
- for (SlashCommand cmd : context.toolRegistry() != null
- ? List.
+ * 支持:
+ *
- * 管理用户输入循环、命令分发、Agent 调用和输出渲染。
- * 当前版本使用 Scanner 作为输入方式(Phase 2 会升级到 JLine)。
+ * 使用 JLine 3 提供丰富的终端交互体验:
+ *
+ *
+ */
+public class ClaudeCodeCompleter implements Completer {
+
+ private final CommandRegistry commandRegistry;
+ private final ToolRegistry toolRegistry;
+
+ public ClaudeCodeCompleter(CommandRegistry commandRegistry, ToolRegistry toolRegistry) {
+ this.commandRegistry = commandRegistry;
+ this.toolRegistry = toolRegistry;
+ }
+
+ @Override
+ public void complete(LineReader reader, ParsedLine line, List
+ *
+ * 当 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,62 +78,149 @@ 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"));
+ }
+ }
+
+ /** 打印启动 Banner(JLine 模式) */
+ 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;
- }
+ handleInput(input, cmdContext);
+ }
- // 调用 Agent 循环
- try {
- spinner.start("Thinking...");
- String response = agentLoop.run(input);
- spinner.stop();
+ out.println(AnsiStyle.dim("\n Goodbye! 👋\n"));
+ }
- 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();
- }
+ // ==================== 公共输入处理 ====================
+
+ /** 处理用户输入(命令分发或 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;
}
- out.println(AnsiStyle.dim("\n Goodbye! 👋\n"));
+ // 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() {