From db3de65c3c59b9c041af365ec8a550e8f31c2ef7 Mon Sep 17 00:00:00 2001 From: abel533 Date: Sat, 4 Apr 2026 20:08:08 +0800 Subject: [PATCH] fix: header scrolls with messages, auto-scroll pauses on scroll-up - Header/logo moved from fixed layout to messagesArea first items (matches official LogoHeader behavior: scrolls with messages, not pinned) - addMessageInternal preserves scrollOffset when user has scrolled up (matches official sticky-scroll: only auto-scrolls when at bottom) - Terminal title uses ConsolePatcher.getOriginalOut() to bypass interception Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../claudecode/tui/ClaudeCodeComponent.java | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java b/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java index 978e084..48d505d 100644 --- a/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java +++ b/src/main/java/com/claudecode/tui/ClaudeCodeComponent.java @@ -211,21 +211,12 @@ public class ClaudeCodeComponent extends Component setCursorPosition(cursorRow, cursorCol); } - int headerHeight = 8; // 6 content rows + 2 border lines int bottomHeight = 4 + inputLineCount; int messagePaddingTop = 1; - int maxMessageLines = Math.max(1, h - headerHeight - bottomHeight - messagePaddingTop); - - // 终端高度太小时,隐藏标题框以腾出消息空间 - boolean showHeader = h >= 20; - if (!showHeader) { - maxMessageLines = Math.max(1, h - bottomHeight - messagePaddingTop); - } + // 标题框现在是消息区的一部分,会随消息一起滚动(匹配官方 LogoHeader 行为) + int maxMessageLines = Math.max(1, h - bottomHeight - messagePaddingTop); List layout = new ArrayList<>(); - if (showHeader) { - layout.add(headerBox(w)); - } layout.add(messagesArea(s, maxMessageLines)); layout.add(Spacer.create()); layout.add(statusBar(w, h)); @@ -238,8 +229,8 @@ public class ClaudeCodeComponent extends Component .flexDirection(FlexDirection.COLUMN).width(w).height(h); } - /** 标题框 — 保留原始 ASCII Logo 样式(双列布局) */ - private Renderable headerBox(int w) { + /** 标题框行列表 — ASCII Logo + 信息(作为消息区首部随消息滚动) */ + private List headerLines() { // ASCII 冒烟咖啡杯 String[] logo = { " ) ) ) ", @@ -250,8 +241,6 @@ public class ClaudeCodeComponent extends Component " ╰─┬────┬─╯ " }; int logoWidth = 19; - int sepWidth = 3; // " │ " - int rightWidth = Math.max(0, w - 4 - logoWidth - sepWidth - 2); // 构建右侧信息行(带颜色高亮) @SuppressWarnings("unchecked") @@ -293,19 +282,17 @@ public class ClaudeCodeComponent extends Component rightPart )); } - - return Box.of(rows.toArray(new Renderable[0])) - .flexDirection(FlexDirection.COLUMN) - .borderStyle(BorderStyle.ROUND) - .borderColor(Color.BRIGHT_MAGENTA) - .paddingX(1) - .width(w); + return rows; } /** 消息列表(带虚拟滚动) */ private Renderable messagesArea(TuiState s, int maxLines) { List allItems = new ArrayList<>(); + // 标题框内容(作为消息区首部,随消息一起滚动 — 匹配官方 LogoHeader 行为) + allItems.addAll(headerLines()); + allItems.add(Text.of(" ")); // 空行分隔 + // 初始提示消息 allItems.add(Text.of( Text.of("● ").color(Color.BRIGHT_BLUE), @@ -1219,8 +1206,12 @@ public class ClaudeCodeComponent extends Component private void addMessageInternal(UIMessage msg, TuiState s) { List newMsgs = new ArrayList<>(s.messages); newMsgs.add(msg); + // 匹配官方 sticky-scroll 行为: + // - 用户在底部(scrollOffset=0)→ 新消息保持在底部 + // - 用户已上滚(scrollOffset>0)→ 保持当前位置,不自动跳到底部 + int newOffset = s.scrollOffset; setState(new TuiState(s.inputText, Collections.unmodifiableList(newMsgs), - 0, s.thinking, s.thinkingText)); + newOffset, s.thinking, s.thinkingText)); } /** 追加 token 到当前流式助手消息 */ @@ -1503,15 +1494,18 @@ public class ClaudeCodeComponent extends Component /** * 设置终端标题(OSC 0 escape sequence)。 * 匹配官方 Claude Code 的 useTerminalTitle hook 行为。 - * Windows 使用 ANSI OSC 0,兼容 Windows Terminal / ConEmu 等现代终端。 + * 必须绕过 jink 的 ConsolePatcher(它会拦截 System.out/err), + * 直接写入原始输出流。 */ private static void setTerminalTitle(String title) { if (title == null || title.isBlank()) return; try { + // 绕过 ConsolePatcher 拦截,直接写入终端 + PrintStream raw = io.mybatis.jink.util.ConsolePatcher.getOriginalOut(); // OSC 0: Set window title and icon name // Format: ESC ] 0 ; BEL - System.err.print("\033]0;" + title + "\007"); - System.err.flush(); + raw.print("\033]0;" + title + "\007"); + raw.flush(); } catch (Exception ignored) {} }