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>
This commit is contained in:
@@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user