fix: markdown ####, Ctrl+C exit bug, terminal title
- MarkdownToText: support heading levels 1-6 (was 1-3, causing #### raw display) - Ctrl+C: agent cancel no longer starts exit window (matching official double-press flow) - Terminal title: dynamic OSC 0 title with animated spinner during work, static idle prefix - Matches official useTerminalTitle hook + AnimatedTerminalTitle component - Session title inferred from first user message (simplified vs AI generation) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -80,8 +80,13 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
||||
|
||||
/** 思考动画帧 */
|
||||
private static final String[] SPINNER_FRAMES = {"◐", "◓", "◑", "◒"};
|
||||
/** 终端标题动画帧(匹配官方 TITLE_ANIMATION_FRAMES) */
|
||||
private static final String[] TITLE_ANIMATION_FRAMES = {"⠂", "⠐"};
|
||||
private static final String TITLE_STATIC_PREFIX = "✳";
|
||||
private volatile int spinnerFrame = 0;
|
||||
private volatile Thread spinnerThread;
|
||||
/** 终端标题(从首条用户消息推断) */
|
||||
private volatile String sessionTitle = null;
|
||||
|
||||
/** 权限确认回调(由权限请求设置,用户输入后调用) */
|
||||
private volatile Consumer<String> permissionCallback;
|
||||
@@ -137,6 +142,8 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
||||
this.toolCount = toolCount;
|
||||
this.tokenTracker = tokenTracker;
|
||||
this.onExit = onExit;
|
||||
// 设置初始终端标题(匹配官方 process.title = 'claude')
|
||||
setTerminalTitle(TITLE_STATIC_PREFIX + " Claude Code");
|
||||
}
|
||||
|
||||
// ==================== 渲染 ====================
|
||||
@@ -722,10 +729,10 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
||||
// Ctrl+C: 中断 Agent 或双击退出
|
||||
if (key.ctrl() && "c".equals(input)) {
|
||||
if (agentRunning.get()) {
|
||||
// Agent 运行中 → 取消任务
|
||||
// Agent 运行中 → 仅取消任务,不启动退出窗口
|
||||
// 官方行为:中断和退出是独立流程,中断不影响 double-press-to-exit
|
||||
agentLoop.cancel();
|
||||
addMessageInternal(new SystemMsg("^C (interrupt)", Color.BRIGHT_YELLOW), s);
|
||||
lastCtrlCTime = System.currentTimeMillis();
|
||||
} else {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastCtrlCTime < CTRL_C_EXIT_WINDOW_MS) {
|
||||
@@ -1114,6 +1121,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
||||
onFirstUserInput.accept(text);
|
||||
onFirstUserInput = null; // 只触发一次
|
||||
}
|
||||
inferSessionTitle(text); // 从首条用户消息推断终端标题
|
||||
addMessage(new UserMsg(text));
|
||||
setState(new TuiState("", getState().messages, 0, true, ""));
|
||||
runAgent(text);
|
||||
@@ -1158,11 +1166,19 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
||||
synchronized (spinnerLock) {
|
||||
stopSpinnerInternal();
|
||||
spinnerFrame = 0;
|
||||
setTerminalTitle(computeTerminalTitle()); // 立即更新标题
|
||||
Thread t = Thread.startVirtualThread(() -> {
|
||||
try {
|
||||
int titleUpdateCounter = 0;
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
Thread.sleep(120);
|
||||
spinnerFrame++;
|
||||
titleUpdateCounter++;
|
||||
// 每 ~960ms 更新一次终端标题(匹配官方 TITLE_ANIMATION_INTERVAL_MS=960)
|
||||
if (titleUpdateCounter >= 8) {
|
||||
titleUpdateCounter = 0;
|
||||
setTerminalTitle(computeTerminalTitle());
|
||||
}
|
||||
synchronized (stateLock) {
|
||||
TuiState s = getState();
|
||||
setState(new TuiState(s.inputText, s.messages, s.scrollOffset, s.thinking, s.thinkingText));
|
||||
@@ -1179,6 +1195,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
||||
synchronized (spinnerLock) {
|
||||
stopSpinnerInternal();
|
||||
}
|
||||
setTerminalTitle(computeTerminalTitle()); // 恢复静态标题
|
||||
}
|
||||
|
||||
private void stopSpinnerInternal() {
|
||||
@@ -1480,4 +1497,50 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
||||
} catch (Exception ignored) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== 终端标题 ====================
|
||||
|
||||
/**
|
||||
* 设置终端标题(OSC 0 escape sequence)。
|
||||
* 匹配官方 Claude Code 的 useTerminalTitle hook 行为。
|
||||
* Windows 使用 ANSI OSC 0,兼容 Windows Terminal / ConEmu 等现代终端。
|
||||
*/
|
||||
private static void setTerminalTitle(String title) {
|
||||
if (title == null || title.isBlank()) return;
|
||||
try {
|
||||
// OSC 0: Set window title and icon name
|
||||
// Format: ESC ] 0 ; <title> BEL
|
||||
System.err.print("\033]0;" + title + "\007");
|
||||
System.err.flush();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前状态生成终端标题文本。
|
||||
* 匹配官方 AnimatedTerminalTitle 组件的行为:
|
||||
* - 空闲: "✳ Claude Code" 或 "✳ <sessionTitle>"
|
||||
* - 工作中: "⠂ <title>" / "⠐ <title>" (交替动画)
|
||||
*/
|
||||
private String computeTerminalTitle() {
|
||||
String title = sessionTitle != null ? sessionTitle : "Claude Code";
|
||||
if (agentRunning.get()) {
|
||||
String frame = TITLE_ANIMATION_FRAMES[spinnerFrame % TITLE_ANIMATION_FRAMES.length];
|
||||
return frame + " " + title;
|
||||
}
|
||||
return TITLE_STATIC_PREFIX + " " + title;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从首条用户消息推断会话标题(简化版,不调用 AI)。
|
||||
* 官方使用 Haiku 生成 3-7 词标题,这里取前 40 字符作为简化实现。
|
||||
*/
|
||||
private void inferSessionTitle(String userInput) {
|
||||
if (sessionTitle != null || userInput == null || userInput.isBlank()) return;
|
||||
if (userInput.startsWith("/")) return; // 跳过斜杠命令
|
||||
String trimmed = userInput.strip();
|
||||
if (trimmed.length() > 40) {
|
||||
trimmed = trimmed.substring(0, 40) + "…";
|
||||
}
|
||||
sessionTitle = trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import java.util.regex.Pattern;
|
||||
*/
|
||||
public class MarkdownToText {
|
||||
|
||||
private static final Pattern HEADER_PATTERN = Pattern.compile("^(#{1,3})\\s+(.+)$");
|
||||
private static final Pattern HEADER_PATTERN = Pattern.compile("^(#{1,6})\\s+(.+)$");
|
||||
private static final Pattern BOLD_PATTERN = Pattern.compile("\\*\\*(.+?)\\*\\*");
|
||||
private static final Pattern INLINE_CODE_PATTERN = Pattern.compile("`([^`]+)`");
|
||||
private static final Pattern UNORDERED_LIST_PATTERN = Pattern.compile("^\\s*[-*]\\s+(.+)$");
|
||||
|
||||
Reference in New Issue
Block a user