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>
pull/1/head
abel533 1 month ago
parent d63a10c221
commit 8946ca93f2
  1. 28
      src/main/java/com/claudecode/tui/ClaudeCodeComponent.java
  2. 1
      src/main/java/com/claudecode/tui/JinkReplSession.java

@ -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(),

Loading…
Cancel
Save