@@ -69,6 +69,13 @@ type Overlay struct {
69 // dialog was opened via OpenDialogWithGrace.
69 // dialog was opened via OpenDialogWithGrace.
70 graceOpenedAt time.Time
70 graceOpenedAt time.Time
71 graceLastInputAt time.Time
71 graceLastInputAt time.Time
73+ // Track recently closed dialog IDs so that reopening the same
74+ // dialog type can skip the grace period. This prevents rapid
75+ // successive dialogs (e.g. multiple permission prompts) from
76+ // each eating a keystroke.
78+ lastClosedAt time.Time
74// NewOverlay creates a new [Overlay] instance.
81// NewOverlay creates a new [Overlay] instance.
@@ -106,9 +113,21 @@ func (d *Overlay) OpenDialog(dialog Dialog) {
106// comes first. Use this for dialogs that open asynchronously (e.g.
113// comes first. Use this for dialogs that open asynchronously (e.g.
107// permission prompts) where in-flight keystrokes from a previously
114// permission prompts) where in-flight keystrokes from a previously
108// focused component could act on the dialog before the user sees it.
115// focused component could act on the dialog before the user sees it.
117+// If the same dialog ID was just closed (within reopenGraceWindow),
118+// the grace period is skipped — the user is already focused on this
119+// dialog type and rapid successive prompts should not eat keystrokes.
109func (d *Overlay) OpenDialogWithGrace(dialog Dialog) {
120func (d *Overlay) OpenDialogWithGrace(dialog Dialog) {
110 now := time.Now()
121 now := time.Now()
111 d.dialogs = append(d.dialogs, dialog)
122 d.dialogs = append(d.dialogs, dialog)
124+ // Skip grace when reopening the same dialog type immediately.
125+ if dialog.ID() == d.lastClosedID && now.Sub(d.lastClosedAt) < reopenGraceWindow {
126+ d.graceOpenedAt = time.Time{}
127+ d.graceLastInputAt = time.Time{}
112 d.graceOpenedAt = now
131 d.graceOpenedAt = now
113 d.graceLastInputAt = now
132 d.graceLastInputAt = now
@@ -146,7 +165,15 @@ func (d *Overlay) CloseFrontDialog() {
146 d.removeDialog(len(d.dialogs) - 1)
165 d.removeDialog(len(d.dialogs) - 1)
168+// reopenGraceWindow is how long after closing a dialog we consider
169+// a reopen of the same dialog ID to be "immediate" and skip grace.
170+const reopenGraceWindow = 500 * time.Millisecond
149func (d *Overlay) removeDialog(idx int) {
172func (d *Overlay) removeDialog(idx int) {
173+ if idx == len(d.dialogs)-1 {
174+ d.lastClosedID = d.dialogs[idx].ID()
175+ d.lastClosedAt = time.Now()
150 d.dialogs = append(d.dialogs[:idx], d.dialogs[idx+1:]...)
177 d.dialogs = append(d.dialogs[:idx], d.dialogs[idx+1:]...)
151 // Clear grace state when the front dialog changes.
178 // Clear grace state when the front dialog changes.
152 if idx == len(d.dialogs) {
179 if idx == len(d.dialogs) {