diff --git a/src/main/java/com/claudecode/command/impl/CompactCommand.java b/src/main/java/com/claudecode/command/impl/CompactCommand.java
index b8456ee..6601264 100644
--- a/src/main/java/com/claudecode/command/impl/CompactCommand.java
+++ b/src/main/java/com/claudecode/command/impl/CompactCommand.java
@@ -4,15 +4,34 @@ import com.claudecode.command.CommandContext;
import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle;
import com.claudecode.core.TokenTracker;
+import org.springframework.ai.chat.messages.*;
+import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+
+import java.util.ArrayList;
+import java.util.List;
/**
- * /compact 命令 —— 压缩当前对话上下文。
+ * /compact 命令 —— 用 AI 生成摘要来压缩上下文。
*
* 对应 claude-code/src/commands/compact.ts。
- * 保留系统提示词,用摘要替换详细的对话历史。
+ * 将详细的对话历史替换为 AI 生成的摘要,大幅减少 token 消耗。
+ * 保留系统提示词和最近一轮对话。
*/
public class CompactCommand implements SlashCommand {
+ private static final String COMPACT_PROMPT = """
+ 请将以下对话历史压缩为一段简洁的摘要。要求:
+ 1. 保留所有关键决策、代码变更和技术细节
+ 2. 保留文件路径、函数名等具体信息
+ 3. 保留用户的偏好和要求
+ 4. 省略重复的讨论和无关的细节
+ 5. 用中文输出,控制在500字以内
+
+ 对话历史:
+ """;
+
@Override
public String name() {
return "compact";
@@ -20,38 +39,97 @@ public class CompactCommand implements SlashCommand {
@Override
public String description() {
- return "Compact conversation context";
+ return "用AI摘要压缩对话上下文";
}
@Override
public String execute(String args, CommandContext context) {
if (context.agentLoop() == null) {
- return AnsiStyle.yellow(" ⚠ No active conversation to compact.");
+ return AnsiStyle.yellow(" ⚠ 没有活跃的对话可压缩。");
}
- int before = context.agentLoop().getMessageHistory().size();
+ List history = context.agentLoop().getMessageHistory();
+ int before = history.size();
- if (before <= 2) {
- return AnsiStyle.dim(" Context is already minimal (" + before + " messages). Nothing to compact.");
+ if (before <= 3) {
+ return AnsiStyle.dim(" 上下文已经很小(" + before + " 条消息),无需压缩。");
}
- // 记录压缩前的 token 使用
TokenTracker tracker = context.agentLoop().getTokenTracker();
long tokensBefore = tracker.getInputTokens() + tracker.getOutputTokens();
- // 重置历史(保留系统提示词)
- context.agentLoop().reset();
+ // 尝试用 AI 生成摘要
+ String summary = generateSummary(context, history);
+
+ // 构建压缩后的历史:系统提示 + 摘要作为系统消息 + 保留最后一轮对话
+ List compacted = new ArrayList<>();
+ compacted.add(history.getFirst()); // 原始系统提示词
+
+ if (summary != null && !summary.isBlank()) {
+ compacted.add(new SystemMessage("[对话历史摘要] " + summary));
+ }
+
+ // 保留最后一轮用户消息和助手回复(如果有)
+ for (int i = Math.max(1, before - 2); i < before; i++) {
+ compacted.add(history.get(i));
+ }
- int after = context.agentLoop().getMessageHistory().size();
+ context.agentLoop().replaceHistory(compacted);
+ int after = compacted.size();
StringBuilder sb = new StringBuilder();
- sb.append(AnsiStyle.green(" ✅ Context compacted")).append("\n");
- sb.append(" Messages: ").append(before).append(" → ").append(after).append("\n");
+ sb.append(AnsiStyle.green(" ✅ 上下文已压缩")).append("\n");
+ sb.append(" 消息数: ").append(before).append(" → ").append(after).append("\n");
if (tokensBefore > 0) {
- sb.append(" Tokens used before compact: ").append(TokenTracker.formatTokens(tokensBefore)).append("\n");
+ sb.append(" 压缩前累计 Token: ").append(TokenTracker.formatTokens(tokensBefore)).append("\n");
+ }
+ if (summary != null) {
+ sb.append(AnsiStyle.dim(" 📝 AI 摘要已生成并注入上下文"));
+ } else {
+ sb.append(AnsiStyle.dim(" ⚠ AI 摘要生成失败,仅保留最近对话"));
}
- sb.append(AnsiStyle.dim(" Conversation history cleared. System prompt retained."));
return sb.toString();
}
+
+ /** 调用 AI 生成对话摘要 */
+ private String generateSummary(CommandContext context, List history) {
+ try {
+ ChatModel chatModel = context.agentLoop().getChatModel();
+
+ // 构建摘要请求的消息列史
+ StringBuilder dialogText = new StringBuilder();
+ for (Message msg : history) {
+ switch (msg) {
+ case UserMessage um -> dialogText.append("[用户] ").append(um.getText()).append("\n");
+ case AssistantMessage am -> {
+ if (am.getText() != null && !am.getText().isBlank()) {
+ // 截断过长的助手回复
+ String text = am.getText();
+ if (text.length() > 500) text = text.substring(0, 500) + "...";
+ dialogText.append("[助手] ").append(text).append("\n");
+ }
+ if (am.hasToolCalls()) {
+ for (var tc : am.getToolCalls()) {
+ dialogText.append("[工具调用] ").append(tc.name()).append("\n");
+ }
+ }
+ }
+ default -> {} // 跳过系统消息和工具响应
+ }
+ }
+
+ if (dialogText.isEmpty()) return null;
+
+ Prompt summaryPrompt = new Prompt(List.of(
+ new UserMessage(COMPACT_PROMPT + dialogText)
+ ));
+
+ ChatResponse response = chatModel.call(summaryPrompt);
+ return response.getResult().getOutput().getText();
+ } catch (Exception e) {
+ // 摘要生成失败不影响压缩操作
+ return null;
+ }
+ }
}
diff --git a/src/main/java/com/claudecode/repl/ReplSession.java b/src/main/java/com/claudecode/repl/ReplSession.java
index 9354e9c..06179c0 100644
--- a/src/main/java/com/claudecode/repl/ReplSession.java
+++ b/src/main/java/com/claudecode/repl/ReplSession.java
@@ -114,35 +114,33 @@ public class ReplSession {
.streams(System.in, System.out)
.build()) {
- // 检测是否为 dumb 终端并提示
boolean isDumb = "dumb".equals(terminal.getType());
if (isDumb) {
log.info("当前为 dumb 终端模式,建议使用 Windows Terminal / PowerShell / cmd 获得完整体验");
}
+ // 配置 Parser:支持反斜杠续行 (\) 和 三引号块 (""")
+ DefaultParser parser = new DefaultParser();
+ parser.setEscapeChars(new char[]{'\\'}); // 反斜杠续行
+
LineReader reader = LineReaderBuilder.builder()
.terminal(terminal)
- .parser(new DefaultParser())
+ .parser(parser)
.completer(new ClaudeCodeCompleter(commandRegistry, toolRegistry))
.variable(LineReader.HISTORY_FILE, historyDir.resolve("history"))
.variable(LineReader.HISTORY_SIZE, 1000)
+ .variable(LineReader.SECONDARY_PROMPT_PATTERN, "%P ... ")
.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);
@@ -152,12 +150,10 @@ public class ReplSession {
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;
}