feat: Ctrl+C 双击退出(匹配原版 Claude Code 行为)

- 第一次 Ctrl+C: Agent 运行中则取消任务,空闲时清空输入
- 第二次 Ctrl+C (2秒内): 退出应用
- 快捷键栏显示 'Press Ctrl-C again to exit' 黄色提示
- 2秒超时后自动清除提示(虚拟线程定时)
- 移除未使用的 cmdCount 字段
- 保留 Esc 中断和 Ctrl+D 退出作为备选

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pull/1/head
abel533 1 month ago
parent a20dffa7a0
commit 5ad2628b1a
  1. 39
      src/main/java/com/claudecode/tui/ClaudeCodeComponent.java
  2. 1
      src/main/java/com/claudecode/tui/JinkReplSession.java

@ -67,7 +67,6 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
private final String model; private final String model;
private final String baseUrl; private final String baseUrl;
private final int toolCount; private final int toolCount;
private final int cmdCount;
private final TokenTracker tokenTracker; private final TokenTracker tokenTracker;
private final Runnable onExit; private final Runnable onExit;
@ -97,6 +96,10 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
private volatile int lastRenderedItemCount = 0; private volatile int lastRenderedItemCount = 0;
private volatile int lastMaxVisibleLines = 20; private volatile int lastMaxVisibleLines = 20;
/** Ctrl+C 双击退出:上次按下时间 */
private volatile long lastCtrlCTime = 0;
private static final long CTRL_C_EXIT_WINDOW_MS = 2000; // 2秒内再按一次退出
/** 首次用户输入回调(用于 conversation summary) */ /** 首次用户输入回调(用于 conversation summary) */
private Consumer<String> onFirstUserInput; private Consumer<String> onFirstUserInput;
@ -104,7 +107,7 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
CommandRegistry commandRegistry, CommandRegistry commandRegistry,
ToolRegistry toolRegistry, ToolRegistry toolRegistry,
String provider, String model, String baseUrl, String provider, String model, String baseUrl,
int toolCount, int cmdCount, int toolCount,
TokenTracker tokenTracker, TokenTracker tokenTracker,
Runnable onExit) { Runnable onExit) {
super(TuiState.empty()); super(TuiState.empty());
@ -115,7 +118,6 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
this.model = model; this.model = model;
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.toolCount = toolCount; this.toolCount = toolCount;
this.cmdCount = cmdCount;
this.tokenTracker = tokenTracker; this.tokenTracker = tokenTracker;
this.onExit = onExit; this.onExit = onExit;
} }
@ -539,8 +541,14 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
} }
} }
// Ctrl+C 双击退出提示
boolean ctrlCPending = (System.currentTimeMillis() - lastCtrlCTime) < CTRL_C_EXIT_WINDOW_MS;
Renderable leftText = ctrlCPending
? Text.of("Press Ctrl-C again to exit").color(Color.BRIGHT_YELLOW)
: Text.of("↑↓ history Esc interrupt").dimmed();
return Box.of( return Box.of(
Text.of("↑↓ history Esc interrupt Ctrl+D exit").dimmed(), leftText,
Spacer.create(), Spacer.create(),
Text.of(tokenInfo).color(Color.BRIGHT_GREEN) Text.of(tokenInfo).color(Color.BRIGHT_GREEN)
).paddingX(1).height(1); ).paddingX(1).height(1);
@ -565,13 +573,34 @@ public class ClaudeCodeComponent extends Component<ClaudeCodeComponent.TuiState>
return; return;
} }
// Ctrl+C: 取消当前输入或中断 Agent // Ctrl+C: 中断 Agent 或双击退出
if (key.ctrl() && "c".equals(input)) { if (key.ctrl() && "c".equals(input)) {
if (agentRunning.get()) { if (agentRunning.get()) {
// Agent 运行中 → 取消任务
agentLoop.cancel(); agentLoop.cancel();
addMessageInternal(new SystemMsg("^C (interrupt)", Color.BRIGHT_YELLOW), s); addMessageInternal(new SystemMsg("^C (interrupt)", Color.BRIGHT_YELLOW), s);
lastCtrlCTime = System.currentTimeMillis();
} else { } else {
long now = System.currentTimeMillis();
if (now - lastCtrlCTime < CTRL_C_EXIT_WINDOW_MS) {
// 第二次 Ctrl+C → 退出
if (onExit != null) onExit.run();
} else {
// 第一次 Ctrl+C → 清空输入 + 提示再按一次退出
lastCtrlCTime = now;
setState(new TuiState("", s.messages, s.scrollOffset, false, "")); setState(new TuiState("", s.messages, s.scrollOffset, false, ""));
// 启动定时器,超时后清除提示
Thread.startVirtualThread(() -> {
try { Thread.sleep(CTRL_C_EXIT_WINDOW_MS); } catch (InterruptedException ignored) {}
// 超时后刷新显示(清除 "Press Ctrl-C again to exit" 提示)
synchronized (stateLock) {
if (System.currentTimeMillis() - lastCtrlCTime >= CTRL_C_EXIT_WINDOW_MS) {
TuiState cur = getState();
setState(new TuiState(cur.inputText, cur.messages, cur.scrollOffset, cur.thinking, cur.thinkingText));
}
}
});
}
} }
return; return;
} }

@ -79,7 +79,6 @@ public class JinkReplSession {
providerInfo.model(), providerInfo.model(),
providerInfo.baseUrl(), providerInfo.baseUrl(),
toolRegistry.size(), toolRegistry.size(),
commandRegistry.getCommands().size(),
tokenTracker, tokenTracker,
this::exit this::exit
); );

Loading…
Cancel
Save