feat: P1功能增强 - 3命令+代码高亮+状态行
新增3个命令: - /resume: 恢复已保存的对话(支持list/序号选择) - /export: 导出对话为Markdown文件 - /commit: 创建Git commit(支持AI生成commit message) 代码语法高亮(MarkdownRenderer增强): - 支持Java/JS/TS/Python/Bash/SQL关键字着色 - 字符串字面量黄色、数字紫色、注释灰色斜体 - 注解(@Annotation)亮黄色 - true/false/null 红色 - 新增引用块、有序列表、复选框、链接渲染 - 代码块边框对齐 底部状态行(StatusLine): - 模型名、Token用量、费用、API调用次数、工作目录 - 非dumb终端自动启用 - 每次Agent循环后刷新显示 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import org.springframework.ai.chat.messages.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* /export 命令 —— 将对话历史导出为 Markdown 文件。
|
||||
* <p>
|
||||
* 支持格式:
|
||||
* <ul>
|
||||
* <li>/export —— 导出到当前目录的 conversation-时间戳.md</li>
|
||||
* <li>/export [路径] —— 导出到指定路径</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class ExportCommand implements SlashCommand {
|
||||
|
||||
private static final DateTimeFormatter TIMESTAMP_FMT = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "export";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Export conversation to Markdown file";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
List<Message> history = context.agentLoop().getMessageHistory();
|
||||
|
||||
// 至少需要系统提示 + 用户消息 + 助手回复
|
||||
if (history.size() < 3) {
|
||||
return AnsiStyle.yellow(" ⚠ 暂无足够的对话内容可导出");
|
||||
}
|
||||
|
||||
// 确定输出路径
|
||||
args = args == null ? "" : args.strip();
|
||||
Path outputPath;
|
||||
if (!args.isEmpty()) {
|
||||
outputPath = Path.of(args);
|
||||
if (!outputPath.isAbsolute()) {
|
||||
outputPath = Path.of(System.getProperty("user.dir")).resolve(outputPath);
|
||||
}
|
||||
} else {
|
||||
String timestamp = TIMESTAMP_FMT.format(LocalDateTime.now());
|
||||
outputPath = Path.of(System.getProperty("user.dir"), "conversation-" + timestamp + ".md");
|
||||
}
|
||||
|
||||
// 生成 Markdown 内容
|
||||
String markdown = generateMarkdown(history);
|
||||
|
||||
try {
|
||||
Files.createDirectories(outputPath.getParent());
|
||||
Files.writeString(outputPath, markdown, StandardCharsets.UTF_8);
|
||||
|
||||
int msgCount = history.size();
|
||||
int lineCount = (int) markdown.lines().count();
|
||||
return AnsiStyle.green(" ✓ 对话已导出: " + outputPath)
|
||||
+ AnsiStyle.dim(" (" + msgCount + " messages, " + lineCount + " lines)");
|
||||
} catch (IOException e) {
|
||||
return AnsiStyle.red(" ✗ 导出失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String generateMarkdown(List<Message> history) {
|
||||
StringBuilder md = new StringBuilder();
|
||||
md.append("# Claude Code Java - Conversation Export\n\n");
|
||||
md.append("- **Exported at**: ").append(LocalDateTime.now()).append("\n");
|
||||
md.append("- **Working directory**: ").append(System.getProperty("user.dir")).append("\n");
|
||||
md.append("- **Messages**: ").append(history.size()).append("\n\n");
|
||||
md.append("---\n\n");
|
||||
|
||||
for (Message msg : history) {
|
||||
switch (msg) {
|
||||
case SystemMessage sm -> {
|
||||
md.append("## 🔧 System Prompt\n\n");
|
||||
md.append("<details>\n<summary>Click to expand system prompt</summary>\n\n");
|
||||
md.append(sm.getText()).append("\n\n");
|
||||
md.append("</details>\n\n---\n\n");
|
||||
}
|
||||
case UserMessage um -> {
|
||||
md.append("## 👤 User\n\n");
|
||||
md.append(um.getText()).append("\n\n");
|
||||
}
|
||||
case AssistantMessage am -> {
|
||||
md.append("## 🤖 Assistant\n\n");
|
||||
if (am.getText() != null && !am.getText().isBlank()) {
|
||||
md.append(am.getText()).append("\n\n");
|
||||
}
|
||||
if (am.hasToolCalls()) {
|
||||
md.append("### Tool Calls\n\n");
|
||||
for (var tc : am.getToolCalls()) {
|
||||
md.append("- **").append(tc.name()).append("**");
|
||||
if (tc.arguments() != null) {
|
||||
md.append("\n ```json\n ").append(tc.arguments()).append("\n ```");
|
||||
}
|
||||
md.append("\n");
|
||||
}
|
||||
md.append("\n");
|
||||
}
|
||||
}
|
||||
case ToolResponseMessage trm -> {
|
||||
md.append("### 🔨 Tool Results\n\n");
|
||||
for (var resp : trm.getResponses()) {
|
||||
md.append("**").append(resp.name()).append("**:\n");
|
||||
String data = resp.responseData();
|
||||
if (data != null) {
|
||||
// 截断过长的工具输出
|
||||
if (data.length() > 2000) {
|
||||
data = data.substring(0, 2000) + "\n... (truncated)";
|
||||
}
|
||||
md.append("```\n").append(data).append("\n```\n\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
|
||||
return md.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user