Skip to content

Commit 217c282

Browse files
committed
🐛 fix: esc-double-interrupt
1 parent 878acef commit 217c282

2 files changed

Lines changed: 30 additions & 7 deletions

File tree

tui/i18n.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ var translations = map[string]map[Lang]string{
340340
"- `ctrl+j` — 换行(多行输入)\n" +
341341
"- `Ctrl+B` — 显示/隐藏右侧状态栏\n" +
342342
"- `Ctrl+V` — 粘贴(含图片)\n" +
343-
"- `Esc` — 中断当前对话\n" +
343+
"- `Esc` — 中断当前对话(按两次;防 vim 习惯误触。也可 Ctrl+C 单按即停)\n" +
344344
"- `Ctrl+C` — 清空输入框;输入为空时按两次退出程序(1 秒内;弹窗内则关弹窗)",
345345
LangEN: "\n**Slash commands**\n\n" +
346346
"- `/plan` — Switch to read-only mode (Read / List / Grep / Glob / Tree / Search / Fetch / Memory only)\n" +
@@ -374,7 +374,7 @@ var translations = map[string]map[Lang]string{
374374
"- `ctrl+j` — Newline (multi-line input)\n" +
375375
"- `Ctrl+B` — Show/hide the right status panel\n" +
376376
"- `Ctrl+V` — Paste (including images)\n" +
377-
"- `Esc` — Interrupt current turn\n" +
377+
"- `Esc` — Interrupt current turn (press twice; avoids vim-habit misfires. Or Ctrl+C to stop in one press)\n" +
378378
"- `Ctrl+C` — Clear the input; when empty, press twice within 1s to quit (closes modal if open)",
379379
},
380380

@@ -475,7 +475,7 @@ var translations = map[string]map[Lang]string{
475475
// === 输入框上方活动行 / 完成行 ===
476476
// footer 状态词单独成键(不复用右栏紧凑的 status.* 英文 token),这样能给中文模式
477477
// 提供本地化文案。footer.* 的 key 后缀与 m.status 取值一一对应(thinking/streaming/tool)。
478-
"footer.interrupt": {LangZH: "Esc 中断", LangEN: "Esc to interrupt"},
478+
"footer.interrupt": {LangZH: "Esc×2 中断", LangEN: "Esc×2 to interrupt"},
479479
"footer.thinking": {LangZH: "思考中", LangEN: "Thinking"},
480480
"footer.streaming": {LangZH: "输出中", LangEN: "Responding"},
481481
"footer.tool": {LangZH: "调用工具", LangEN: "Running tool"},
@@ -647,9 +647,13 @@ var translations = map[string]map[Lang]string{
647647
LangZH: "再按一次 Ctrl+C 退出 deepx(1 秒内)",
648648
LangEN: "Press Ctrl+C again to quit deepx (within 1 second)",
649649
},
650+
"misc.esc_again_to_interrupt": {
651+
LangZH: "再按一次 Esc 中断生成(1 秒内;或按 Ctrl+C 单按即停)",
652+
LangEN: "Press Esc again to interrupt (within 1s; or Ctrl+C to stop in one press)",
653+
},
650654
"misc.input_placeholder": {
651-
LangZH: "Type a message… Enter 发送 · ctrl+j 换行 · ctrl+c 清空 · Esc 中断",
652-
LangEN: "Type a message… Enter send · ctrl+j newline · ctrl+c clear · Esc interrupt",
655+
LangZH: "Type a message… Enter 发送 · ctrl+j 换行 · ctrl+c 清空 · Esc×2 中断",
656+
LangEN: "Type a message… Enter send · ctrl+j newline · ctrl+c clear · Esc×2 interrupt",
653657
},
654658
"misc.history_suffix": {
655659
LangZH: "_(以上为历史对话,共 %d 条)_",

tui/model.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,10 @@ type model struct {
317317
// streaming 中第一次也会取消流(行为同 Esc),避免要按两个键才能停一个跑得离谱的任务。
318318
lastCtrlCAt time.Time
319319

320+
// Esc 双击中断保护(issue #120):vim/neovim 用户肌肉记忆常误按 Esc。
321+
// 第一次按时间戳记到这里;escInterruptWindow 内再按才真正中断 streaming。
322+
lastEscAt time.Time
323+
320324
// 右栏仪表盘字段
321325
workspace string // os.Getwd() at startup,展示当前工作目录
322326
turnStartedAt time.Time // 本轮 Enter 时刻,用于实时计算 elapsed
@@ -757,6 +761,10 @@ const cursorBlinkInterval = 600 * time.Millisecond
757761
// 提示再按退出;窗口内第二次 → 真退。
758762
const ctrlcExitWindow = 1 * time.Second
759763

764+
// escInterruptWindow 两次 Esc 之间允许的最大时间差。streaming 中第一次按只提示、不中断;
765+
// 窗口内第二次 → 真中断。防止 vim 用户误按一下 Esc 就打断生成(issue #120)。
766+
const escInterruptWindow = 1 * time.Second
767+
760768
func cursorBlinkTick() tea.Cmd {
761769
return tea.Tick(cursorBlinkInterval, func(time.Time) tea.Msg {
762770
return cursorBlinkTickMsg{}
@@ -2143,9 +2151,20 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
21432151
m.refreshViewport()
21442152
return m, nil
21452153
}
2146-
// Esc 中断当前对话。取消 context 真正终止后台 HTTP 请求和工具调用,
2147-
// 然后 drain channel 防止 goroutine 阻塞(与浏览器"停止"共用 interruptStream)。
2154+
// Esc 中断当前对话 —— 双击保护(issue #120):vim/neovim 用户常误按 Esc,
2155+
// 单按一次只提示、不打断;escInterruptWindow 内再按一次才真正中断。
2156+
// 想单按即停的用户仍可用 Ctrl+C(空输入时单按即取消生成)。
21482157
if m.streaming && m.streamCh != nil {
2158+
now := time.Now()
2159+
if m.lastEscAt.IsZero() || now.Sub(m.lastEscAt) > escInterruptWindow {
2160+
// 第一次(或上次已过期):只记时间 + 提示,不中断。
2161+
m.lastEscAt = now
2162+
m.appendChat("System", T("misc.esc_again_to_interrupt"))
2163+
m.refreshViewport()
2164+
return m, nil
2165+
}
2166+
// 窗口内第二次:真正中断。
2167+
m.lastEscAt = time.Time{}
21492168
m = m.interruptStream()
21502169
// 打断后把这一轮的用户输入回填到空输入框,方便改一下重发。
21512170
// pendingUserText 是本轮原文(StreamDoneMsg 成功后才清空,所以打断时仍在);

0 commit comments

Comments
 (0)