feat: dynamic terminal title matching official Claude Code behavior

- Title updates on every user message (not just the first)
- Shows current tool name during execution: '⠂ Bash · user topic'
- Animation pauses during permission prompts (matches isWaitingForApproval)
- Clears tool name when tool completes
- Priority: sessionTitle (from user input) > 'Claude Code' (fallback)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
abel533
2026-04-04 21:28:09 +08:00
co-authored by Copilot
parent d63a10c221
commit 8946ca93f2
2 changed files with 23 additions and 6 deletions
@@ -87,8 +87,10 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
private static final String TITLE_STATIC_PREFIX = "";
private volatile int spinnerFrame = 0;
private volatile Thread spinnerThread;
/** 终端标题(从首条用户消息推断) */
/** 终端标题(从用户消息推断,每次新消息都更新 */
private volatile String sessionTitle = null;
/** 当前正在执行的工具名(用于标题显示) */
private volatile String currentToolName = null;
private volatile boolean titleInitialized = false;
/** 权限确认回调(由权限请求设置,用户输入后调用) */
@@ -1125,7 +1127,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
onFirstUserInput.accept(text);
onFirstUserInput = null; // 只触发一次
}
inferSessionTitle(text); // 从首条用户消息推断终端标题
updateSessionTitle(text); // 从用户消息更新终端标题
addMessage(new UserMsg(text));
setState(new TuiState("", getState().messages, 0, true, ""));
runAgent(text);
@@ -1266,6 +1268,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
/** 更新最后一个工具调用消息的结果 */
public void completeLastToolCall(String result) {
currentToolName = null; // 工具执行完成,清除工具名
synchronized (stateLock) {
TuiState s = getState();
List<UIMessage> msgs = new ArrayList<>(s.messages);
@@ -1300,6 +1303,11 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
}
}
/** 设置当前正在执行的工具名(用于终端标题显示) */
public void setCurrentToolName(String toolName) {
this.currentToolName = toolName;
}
/** 设置简单文本输入回调(用于无选项的 AskUser) */
public void requestTextInput(Consumer<String> callback) {
this.askOptions = null;
@@ -1535,22 +1543,30 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
* 匹配官方 AnimatedTerminalTitle 组件的行为:
* - 空闲: "✳ Claude Code" 或 "✳ <sessionTitle>"
* - 工作中: "⠂ <title>" / "⠐ <title>" (交替动画)
* - 等待审批: "✳ <title>" (暂停动画)
* - 工具执行中: "⠂ <toolName> · <title>"
*/
private String computeTerminalTitle() {
String title = sessionTitle != null ? sessionTitle : "Claude Code";
if (agentRunning.get()) {
boolean isWaiting = permissionCallback != null;
if (agentRunning.get() && !isWaiting) {
String frame = TITLE_ANIMATION_FRAMES[spinnerFrame % TITLE_ANIMATION_FRAMES.length];
String toolName = currentToolName;
if (toolName != null && !toolName.isEmpty()) {
return frame + " " + toolName + " · " + title;
}
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;
private void updateSessionTitle(String userInput) {
if (userInput == null || userInput.isBlank()) return;
if (userInput.startsWith("/")) return; // 跳过斜杠命令
String trimmed = userInput.strip();
if (trimmed.length() > 40) {
@@ -116,6 +116,7 @@ public class JinkReplSession {
case START -> {
// 完成当前流式消息(如果有)
finishCurrentStreaming();
component.setCurrentToolName(event.toolName());
component.addMessage(new ToolCallMsg(
event.toolName(),
event.arguments(),