enhance: improve header colors, streaming markdown, scroll preservation

- Header: use colored values (API cyan, model green, dir yellow, etc.)
- Markdown: render progressively during streaming (not just after completion)
  - Streaming cursor appended to last rendered line
- Scroll: preserve user scroll offset during streaming token updates
  - Previously reset to 0 on every token, now keeps user position
- All process info (tool calls, thinking) persists after completion

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pull/1/head
abel533 1 month ago
parent 9a437889b7
commit 2f645b9d85
  1. 96
      src/main/java/com/claudecode/tui/ClaudeCodeComponent.java

@ -158,37 +158,48 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
" │ CODE │─╯ ", " │ CODE │─╯ ",
" ╰─┬────┬─╯ " " ╰─┬────┬─╯ "
}; };
// 右侧信息
String[] info = {
"",
"Welcome!",
"API: " + baseUrl,
"Protocol: " + provider.toUpperCase() + " Model: " + model,
"Work Dir: " + System.getProperty("user.dir", "."),
"Tools: " + toolCount + " | Commands: " + cmdCount
};
// 构建双列文本行
int logoWidth = 19; int logoWidth = 19;
int sepWidth = 3; // " │ " int sepWidth = 3; // " │ "
int rightWidth = Math.max(0, w - 4 - logoWidth - sepWidth - 2); // 4=border+padding int rightWidth = Math.max(0, w - 4 - logoWidth - sepWidth - 2);
// 构建右侧信息行(带颜色高亮)
@SuppressWarnings("unchecked")
Renderable[] rightTexts = {
Text.of(""),
Text.of("Welcome!").bold(),
Text.of(
Text.of("API: ").dimmed(),
Text.of(baseUrl).color(Color.BRIGHT_CYAN)
),
Text.of(
Text.of("Protocol: ").dimmed(),
Text.of(provider.toUpperCase()).color(Color.BRIGHT_GREEN),
Text.of(" Model: ").dimmed(),
Text.of(model).color(Color.BRIGHT_GREEN)
),
Text.of(
Text.of("Work Dir: ").dimmed(),
Text.of(System.getProperty("user.dir", ".")).color(Color.BRIGHT_YELLOW)
),
Text.of(
Text.of("Tools: ").dimmed(),
Text.of(String.valueOf(toolCount)).color(Color.BRIGHT_CYAN),
Text.of(" │ Commands: ").dimmed(),
Text.of(String.valueOf(cmdCount)).color(Color.BRIGHT_CYAN)
)
};
List<Renderable> rows = new ArrayList<>(); List<Renderable> rows = new ArrayList<>();
int maxRows = Math.max(logo.length, info.length); int maxRows = Math.max(logo.length, rightTexts.length);
for (int i = 0; i < maxRows; i++) { for (int i = 0; i < maxRows; i++) {
String left = i < logo.length ? logo[i] : ""; String left = i < logo.length ? logo[i] : "";
String right = i < info.length ? info[i] : "";
// 补齐左侧
if (left.length() < logoWidth) left = left + " ".repeat(logoWidth - left.length()); if (left.length() < logoWidth) left = left + " ".repeat(logoWidth - left.length());
// 截断右侧 Renderable rightPart = i < rightTexts.length ? rightTexts[i] : Text.of("");
if (right.length() > rightWidth) right = right.substring(0, rightWidth);
// 右侧补齐
right = right + " ".repeat(Math.max(0, rightWidth - right.length()));
rows.add(Text.of( rows.add(Text.of(
Text.of(left).color(Color.BRIGHT_CYAN), Text.of(left).color(Color.BRIGHT_CYAN),
Text.of(" │ ").dimmed(), Text.of(" │ ").dimmed(),
i == 1 ? Text.of(right).bold() : Text.of(right).dimmed() rightPart
)); ));
} }
@ -259,34 +270,18 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
yield lines; yield lines;
} }
if (!m.streaming()) { // 始终使用 Markdown 渲染(流式和完成都渲染)
// 已完成的消息 — 使用 Markdown 渲染 List<Renderable> mdLines = MarkdownToText.convert(text);
List<Renderable> mdLines = MarkdownToText.convert(text); // 流式时在最后一行追加光标
for (int i = 0; i < mdLines.size(); i++) { if (m.streaming() && !mdLines.isEmpty()) {
if (i == 0) { Renderable lastLine = mdLines.getLast();
lines.add(Text.of(Text.of("● ").color(Color.BRIGHT_CYAN), mdLines.get(i))); mdLines.set(mdLines.size() - 1, Text.of(lastLine, Text.of("▌").color(Color.BRIGHT_CYAN)));
} else { }
lines.add(Text.of(Text.of(" "), mdLines.get(i))); for (int i = 0; i < mdLines.size(); i++) {
} if (i == 0) {
} lines.add(Text.of(Text.of("● ").color(Color.BRIGHT_CYAN), mdLines.get(i)));
} else { } else {
// 流式中 — 直接按行显示带光标 lines.add(Text.of(Text.of(" "), mdLines.get(i)));
String[] textLines = text.split("\n", -1);
for (int i = 0; i < textLines.length; i++) {
String line = textLines[i];
String displayLine = (m.streaming() && i == textLines.length - 1)
? line + "▌" : line;
if (i == 0) {
lines.add(Text.of(
Text.of("● ").color(Color.BRIGHT_CYAN),
Text.of(displayLine).color(Color.WHITE)
));
} else {
lines.add(Text.of(
Text.of(" ").dimmed(),
Text.of(displayLine).color(Color.WHITE)
));
}
} }
} }
yield lines; yield lines;
@ -698,8 +693,9 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
msgs.add(new AssistantMsg(token, true)); msgs.add(new AssistantMsg(token, true));
} }
// 保留用户的滚动偏移(如果用户手动滚动过则不自动归零)
setState(new TuiState(s.inputText, Collections.unmodifiableList(msgs), setState(new TuiState(s.inputText, Collections.unmodifiableList(msgs),
0, s.thinking, s.thinkingText)); s.scrollOffset, s.thinking, s.thinkingText));
} }
} }
@ -712,7 +708,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
if (!msgs.isEmpty() && msgs.getLast() instanceof AssistantMsg am && am.streaming()) { if (!msgs.isEmpty() && msgs.getLast() instanceof AssistantMsg am && am.streaming()) {
msgs.set(msgs.size() - 1, am.finish()); msgs.set(msgs.size() - 1, am.finish());
setState(new TuiState(s.inputText, Collections.unmodifiableList(msgs), setState(new TuiState(s.inputText, Collections.unmodifiableList(msgs),
0, s.thinking, s.thinkingText)); s.scrollOffset, s.thinking, s.thinkingText));
} }
} }
} }

Loading…
Cancel
Save