6
6
"os"
7
7
"os/exec"
8
8
"strings"
9
+ "time"
9
10
10
11
"github.com/charmbracelet/bubbles/v2/key"
11
12
tea "github.com/charmbracelet/bubbletea/v2"
@@ -25,6 +26,19 @@ import (
25
26
"github.com/sst/opencode/pkg/client"
26
27
)
27
28
29
+ // EscapeDebounceTimeoutMsg is sent when the escape key debounce timeout expires
30
+ type EscapeDebounceTimeoutMsg struct {}
31
+
32
+ // EscapeKeyState tracks the state of escape key presses for debouncing
33
+ type EscapeKeyState int
34
+
35
+ const (
36
+ EscapeKeyIdle EscapeKeyState = iota
37
+ EscapeKeyFirstPress
38
+ )
39
+
40
+ const escapeDebounceTimeout = 1 * time .Second
41
+
28
42
type appModel struct {
29
43
width , height int
30
44
app * app.App
@@ -40,6 +54,7 @@ type appModel struct {
40
54
leaderBinding * key.Binding
41
55
isLeaderSequence bool
42
56
toastManager * toast.ToastManager
57
+ escapeKeyState EscapeKeyState
43
58
}
44
59
45
60
func (a appModel ) Init () tea.Cmd {
@@ -171,9 +186,31 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
171
186
return a , nil
172
187
}
173
188
174
- // 6. Check again for commands that don't require leader
189
+ // 6. Handle escape key debounce for session interrupt
190
+ if keyString == "esc" && a .app .IsBusy () {
191
+ switch a .escapeKeyState {
192
+ case EscapeKeyIdle :
193
+ // First escape press - start debounce timer
194
+ a .escapeKeyState = EscapeKeyFirstPress
195
+ a .editor .SetEscapeKeyInDebounce (true )
196
+ return a , tea .Tick (escapeDebounceTimeout , func (t time.Time ) tea.Msg {
197
+ return EscapeDebounceTimeoutMsg {}
198
+ })
199
+ case EscapeKeyFirstPress :
200
+ // Second escape press within timeout - actually interrupt
201
+ a .escapeKeyState = EscapeKeyIdle
202
+ a .editor .SetEscapeKeyInDebounce (false )
203
+ return a , util .CmdHandler (commands .ExecuteCommandMsg (a .app .Commands [commands .SessionInterruptCommand ]))
204
+ }
205
+ }
206
+
207
+ // 7. Check again for commands that don't require leader (excluding escape when busy)
175
208
matches := a .app .Commands .Matches (msg , a .isLeaderSequence )
176
209
if len (matches ) > 0 {
210
+ // Skip escape key interrupt if we're in debounce mode and app is busy
211
+ if keyString == "esc" && a .app .IsBusy () && a .escapeKeyState != EscapeKeyIdle {
212
+ return a , nil
213
+ }
177
214
return a , util .CmdHandler (commands .ExecuteCommandsMsg (matches ))
178
215
}
179
216
@@ -283,6 +320,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
283
320
tm , cmd := a .toastManager .Update (msg )
284
321
a .toastManager = tm
285
322
cmds = append (cmds , cmd )
323
+ case EscapeDebounceTimeoutMsg :
324
+ // Reset escape key state after timeout
325
+ a .escapeKeyState = EscapeKeyIdle
326
+ a .editor .SetEscapeKeyInDebounce (false )
286
327
}
287
328
288
329
// update status bar
@@ -575,6 +616,7 @@ func NewModel(app *app.App) tea.Model {
575
616
showCompletionDialog : false ,
576
617
editorContainer : editorContainer ,
577
618
toastManager : toast .NewToastManager (),
619
+ escapeKeyState : EscapeKeyIdle ,
578
620
layout : layout .NewFlexLayout (
579
621
[]tea.ViewModel {messagesContainer , editorContainer },
580
622
layout .WithDirection (layout .FlexDirectionVertical ),
0 commit comments