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>
pull/1/head
abel533 1 month ago
parent 4523e7e4d3
commit db3de65c3c
  1. 46
      src/main/java/com/claudecode/tui/ClaudeCodeComponent.java

@ -211,21 +211,12 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
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<Renderable> 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<ClaudeCodeComponent.TuiState>
.flexDirection(FlexDirection.COLUMN).width(w).height(h);
}
/** 标题框 — 保留原始 ASCII Logo 样式(双列布局) */
private Renderable headerBox(int w) {
/** 标题框行列表 — ASCII Logo + 信息(作为消息区首部随消息滚动) */
private List<Renderable> headerLines() {
// ASCII 冒烟咖啡杯
String[] logo = {
" ) ) ) ",
@ -250,8 +241,6 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
" ╰─┬────┬─╯ "
};
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<ClaudeCodeComponent.TuiState>
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<Renderable> 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<ClaudeCodeComponent.TuiState>
private void addMessageInternal(UIMessage msg, TuiState s) {
List<UIMessage> 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<ClaudeCodeComponent.TuiState>
/**
* 设置终端标题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 ; <title> BEL
System.err.print("\033]0;" + title + "\007");
System.err.flush();
raw.print("\033]0;" + title + "\007");
raw.flush();
} catch (Exception ignored) {}
}

Loading…
Cancel
Save