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 = {"◐", "◓", "◑", "◒"};
|
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 int spinnerFrame = 0;
|
||||||
private volatile Thread spinnerThread;
|
private volatile Thread spinnerThread;
|
||||||
|
/** 终端标题(从首条用户消息推断) */
|
||||||
|
private volatile String sessionTitle = null;
|
||||||
|
|
||||||
/** 权限确认回调(由权限请求设置,用户输入后调用) */
|
/** 权限确认回调(由权限请求设置,用户输入后调用) */
|
||||||
private volatile Consumer<String> permissionCallback;
|
private volatile Consumer<String> permissionCallback;
|
||||||
@@ -137,6 +142,8 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
this.toolCount = toolCount;
|
this.toolCount = toolCount;
|
||||||
this.tokenTracker = tokenTracker;
|
this.tokenTracker = tokenTracker;
|
||||||
this.onExit = onExit;
|
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 或双击退出
|
// Ctrl+C: 中断 Agent 或双击退出
|
||||||
if (key.ctrl() && "c".equals(input)) {
|
if (key.ctrl() && "c".equals(input)) {
|
||||||
if (agentRunning.get()) {
|
if (agentRunning.get()) {
|
||||||
// Agent 运行中 → 取消任务
|
// Agent 运行中 → 仅取消任务,不启动退出窗口
|
||||||
|
// 官方行为:中断和退出是独立流程,中断不影响 double-press-to-exit
|
||||||
agentLoop.cancel();
|
agentLoop.cancel();
|
||||||
addMessageInternal(new SystemMsg("^C (interrupt)", Color.BRIGHT_YELLOW), s);
|
addMessageInternal(new SystemMsg("^C (interrupt)", Color.BRIGHT_YELLOW), s);
|
||||||
lastCtrlCTime = System.currentTimeMillis();
|
|
||||||
} else {
|
} else {
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
if (now - lastCtrlCTime < CTRL_C_EXIT_WINDOW_MS) {
|
if (now - lastCtrlCTime < CTRL_C_EXIT_WINDOW_MS) {
|
||||||
@@ -1114,6 +1121,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
onFirstUserInput.accept(text);
|
onFirstUserInput.accept(text);
|
||||||
onFirstUserInput = null; // 只触发一次
|
onFirstUserInput = null; // 只触发一次
|
||||||
}
|
}
|
||||||
|
inferSessionTitle(text); // 从首条用户消息推断终端标题
|
||||||
addMessage(new UserMsg(text));
|
addMessage(new UserMsg(text));
|
||||||
setState(new TuiState("", getState().messages, 0, true, ""));
|
setState(new TuiState("", getState().messages, 0, true, ""));
|
||||||
runAgent(text);
|
runAgent(text);
|
||||||
@@ -1158,11 +1166,19 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
synchronized (spinnerLock) {
|
synchronized (spinnerLock) {
|
||||||
stopSpinnerInternal();
|
stopSpinnerInternal();
|
||||||
spinnerFrame = 0;
|
spinnerFrame = 0;
|
||||||
|
setTerminalTitle(computeTerminalTitle()); // 立即更新标题
|
||||||
Thread t = Thread.startVirtualThread(() -> {
|
Thread t = Thread.startVirtualThread(() -> {
|
||||||
try {
|
try {
|
||||||
|
int titleUpdateCounter = 0;
|
||||||
while (!Thread.currentThread().isInterrupted()) {
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
Thread.sleep(120);
|
Thread.sleep(120);
|
||||||
spinnerFrame++;
|
spinnerFrame++;
|
||||||
|
titleUpdateCounter++;
|
||||||
|
// 每 ~960ms 更新一次终端标题(匹配官方 TITLE_ANIMATION_INTERVAL_MS=960)
|
||||||
|
if (titleUpdateCounter >= 8) {
|
||||||
|
titleUpdateCounter = 0;
|
||||||
|
setTerminalTitle(computeTerminalTitle());
|
||||||
|
}
|
||||||
synchronized (stateLock) {
|
synchronized (stateLock) {
|
||||||
TuiState s = getState();
|
TuiState s = getState();
|
||||||
setState(new TuiState(s.inputText, s.messages, s.scrollOffset, s.thinking, s.thinkingText));
|
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) {
|
synchronized (spinnerLock) {
|
||||||
stopSpinnerInternal();
|
stopSpinnerInternal();
|
||||||
}
|
}
|
||||||
|
setTerminalTitle(computeTerminalTitle()); // 恢复静态标题
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stopSpinnerInternal() {
|
private void stopSpinnerInternal() {
|
||||||
@@ -1480,4 +1497,50 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
|
|||||||
} catch (Exception ignored) {}
|
} catch (Exception ignored) {}
|
||||||
return null;
|
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 {
|
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 BOLD_PATTERN = Pattern.compile("\\*\\*(.+?)\\*\\*");
|
||||||
private static final Pattern INLINE_CODE_PATTERN = Pattern.compile("`([^`]+)`");
|
private static final Pattern INLINE_CODE_PATTERN = Pattern.compile("`([^`]+)`");
|
||||||
private static final Pattern UNORDERED_LIST_PATTERN = Pattern.compile("^\\s*[-*]\\s+(.+)$");
|
private static final Pattern UNORDERED_LIST_PATTERN = Pattern.compile("^\\s*[-*]\\s+(.+)$");
|
||||||
|
|||||||
Reference in New Issue
Block a user