From f995a34d669a65dd511722e35d05f7f0f44c2e9f Mon Sep 17 00:00:00 2001 From: abel533 Date: Sat, 4 Apr 2026 19:06:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=B9=E9=BD=90=E5=AE=98=E6=96=B9?= =?UTF-8?q?=E5=BF=AB=E6=8D=B7=E9=94=AE=E5=92=8C=E5=8A=A8=E7=94=BB=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase A (快速修复): - Ctrl+C 双击窗口从 2000ms 改为 800ms(匹配官方) - 添加 Ctrl+L 强制重绘 - 添加 Ctrl+Home/End 跳到顶部/底部 Phase B (动画改进): - Thinking 显示持续时间: 'Thinking...' → 'Thinking (5s)...' (2秒后开始显示时间,匹配官方行为) - Processing 显示输出 token 计数 - 底部自动滚动已正确实现(新消息 scrollOffset=0) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../claudecode/tui/ClaudeCodeComponent.java | 58 +++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java b/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java index 05290b4..3060bc8 100644 --- a/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java +++ b/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java @@ -102,7 +102,10 @@ public class ClaudeCodeComponent extends Component /** Ctrl+C 双击退出:上次按下时间 */ private volatile long lastCtrlCTime = 0; - private static final long CTRL_C_EXIT_WINDOW_MS = 2000; // 2秒内再按一次退出 + private static final long CTRL_C_EXIT_WINDOW_MS = 800; // 800ms内再按一次退出(匹配官方) + + /** Thinking 开始时间(用于显示耗时) */ + private volatile long thinkingStartTime = 0; /** 首次用户输入回调(用于 conversation summary) */ private Consumer onFirstUserInput; @@ -301,21 +304,28 @@ public class ClaudeCodeComponent extends Component // Thinking / Processing 状态动画(显示在消息区底部) if (agentRunning.get()) { String spinner = SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length]; - if (s.thinking && !s.thinkingText.isEmpty()) { - allItems.add(Text.of( - Text.of(spinner + " ").color(Color.BRIGHT_YELLOW), - Text.of("Thinking...").color(Color.BRIGHT_YELLOW).italic() - )); - } else if (s.thinking) { + if (s.thinking) { + // 计算 thinking 耗时 + long elapsed = thinkingStartTime > 0 + ? (System.currentTimeMillis() - thinkingStartTime) / 1000 + : 0; + String durationText = elapsed >= 2 + ? String.format("Thinking (%ds)...", elapsed) + : "Thinking..."; allItems.add(Text.of( Text.of(spinner + " ").color(Color.BRIGHT_YELLOW), - Text.of("Thinking...").color(Color.BRIGHT_YELLOW).italic() + Text.of(durationText).color(Color.BRIGHT_YELLOW).italic() )); } else { - // Agent 运行中但未进入 thinking(如执行工具、准备调用等) + // Agent 运行中但未进入 thinking(执行工具、流式输出等) + // 显示输出 token 计数 + String tokenInfo = ""; + if (tokenTracker != null && tokenTracker.getOutputTokens() > 0) { + tokenInfo = " (" + tokenTracker.getOutputTokens() + " tokens)"; + } allItems.add(Text.of( Text.of(spinner + " ").color(Color.BRIGHT_CYAN), - Text.of("Processing...").color(Color.BRIGHT_CYAN).italic() + Text.of("Processing..." + tokenInfo).color(Color.BRIGHT_CYAN).italic() )); } } @@ -630,6 +640,12 @@ public class ClaudeCodeComponent extends Component return; } + // Ctrl+L: 强制重绘 + if (key.ctrl() && "l".equals(input)) { + setState(new TuiState(s.inputText, s.messages, s.scrollOffset, s.thinking, s.thinkingText)); + return; + } + // Ctrl+C: 中断 Agent 或双击退出 if (key.ctrl() && "c".equals(input)) { if (agentRunning.get()) { @@ -708,6 +724,12 @@ public class ClaudeCodeComponent extends Component scroll(s, 3); } else if (key.scrollDown()) { scroll(s, -3); + } else if (key.ctrl() && key.home()) { + // Ctrl+Home: 跳到顶部 + scrollToTop(s); + } else if (key.ctrl() && key.end()) { + // Ctrl+End: 跳到底部 + scrollToBottom(s); } else if (key.pageUp()) { scroll(s, 10); } else if (key.pageDown()) { @@ -876,6 +898,8 @@ public class ClaudeCodeComponent extends Component private void handleScrollInput(Key key, TuiState s) { if (key.scrollUp()) scroll(s, 3); else if (key.scrollDown()) scroll(s, -3); + else if (key.ctrl() && key.home()) scrollToTop(s); + else if (key.ctrl() && key.end()) scrollToBottom(s); else if (key.pageUp()) scroll(s, 10); else if (key.pageDown()) scroll(s, -10); } @@ -1110,6 +1134,11 @@ public class ClaudeCodeComponent extends Component /** 设置 thinking 状态 */ public void setThinking(boolean thinking, String text) { + if (thinking && thinkingStartTime == 0) { + thinkingStartTime = System.currentTimeMillis(); + } else if (!thinking) { + thinkingStartTime = 0; + } synchronized (stateLock) { TuiState s = getState(); setState(new TuiState(s.inputText, s.messages, s.scrollOffset, thinking, text)); @@ -1164,6 +1193,15 @@ public class ClaudeCodeComponent extends Component setState(new TuiState(s.inputText, s.messages, newOffset, s.thinking, s.thinkingText)); } + private void scrollToTop(TuiState s) { + int maxOffset = Math.max(0, lastRenderedItemCount - lastMaxVisibleLines); + setState(new TuiState(s.inputText, s.messages, maxOffset, s.thinking, s.thinkingText)); + } + + private void scrollToBottom(TuiState s) { + setState(new TuiState(s.inputText, s.messages, 0, s.thinking, s.thinkingText)); + } + // ==================== 工具方法 ==================== private boolean isPrintableInput(String input, Key key) {