detached c81e580

feat(daemon): serialize per-session command dispatch

commit
c81e58043d099f9c2016784efaea45c6c8cf0926
author
brice · · 2026-07-23 22:04 UTC
parent
dc2f115
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +9 -1
M internal/usecase/daemon/palette.go +4 -1
@@ -303,7 +303,10 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) {
303303 if !ac.closeExecutedPalette(generation, rawQuery) {
304304 return
305305 }
306- if err := cmd.Run(paletteExec{d: d, sess: sess, ac: ac}, args); err != nil {
306+ sess.dispatchMu.Lock()
307+ err := cmd.Run(paletteExec{d: d, sess: sess, ac: ac}, args)
308+ sess.dispatchMu.Unlock()
309+ if err != nil {
307310 d.log.Error("palette command failed", "err", err, "command", cmd.Code)
308311 d.reportError(sess, err)
309312 } else {
@@ -303,7 +303,10 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte) {
303 if !ac.closeExecutedPalette(generation, rawQuery) {303 if !ac.closeExecutedPalette(generation, rawQuery) {
304 return304 return
305 }305 }
306- if err := cmd.Run(paletteExec{d: d, sess: sess, ac: ac}, args); err != nil {306+ sess.dispatchMu.Lock()
307+ err := cmd.Run(paletteExec{d: d, sess: sess, ac: ac}, args)
308+ sess.dispatchMu.Unlock()
309+ if err != nil {
307 d.log.Error("palette command failed", "err", err, "command", cmd.Code)310 d.log.Error("palette command failed", "err", err, "command", cmd.Code)
308 d.reportError(sess, err)311 d.reportError(sess, err)
309 } else {312 } else {
M internal/usecase/daemon/session.go +5 -0
@@ -36,6 +36,11 @@ type session struct {
3636 ctx context.Context
3737 cancel context.CancelFunc
3838
39+ // dispatchMu serializes user-initiated mutating command dispatch (palette
40+ // and vev-cmd control) for this session, so a one-shot command cannot
41+ // resolve focus against one state and mutate a later one. Lock order:
42+ // dispatchMu strictly before d.mu, sess.mu, or any tab mu.
43+ dispatchMu sync.Mutex
3944 mu sync.Mutex // guards tabs, active, client, clipFiles, and clipboard queue state
4045 themeMu sync.Mutex
4146 tabs []*tab
@@ -36,6 +36,11 @@ type session struct {
36 ctx context.Context36 ctx context.Context
37 cancel context.CancelFunc37 cancel context.CancelFunc
38 38
39+ // dispatchMu serializes user-initiated mutating command dispatch (palette
40+ // and vev-cmd control) for this session, so a one-shot command cannot
41+ // resolve focus against one state and mutate a later one. Lock order:
42+ // dispatchMu strictly before d.mu, sess.mu, or any tab mu.
43+ dispatchMu sync.Mutex
39 mu sync.Mutex // guards tabs, active, client, clipFiles, and clipboard queue state44 mu sync.Mutex // guards tabs, active, client, clipFiles, and clipboard queue state
40 themeMu sync.Mutex45 themeMu sync.Mutex
41 tabs []*tab46 tabs []*tab

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close