detached 467ad0a

modes: adapt slide-back chord hint to terminal

The "Press Option+up to slide back into input" hint shown under the
sliding-in queue was correct on Ghostty, iTerm2 (Meta=Option),
Terminal.app (Use Option as Meta), Alacritty, and Kitty -- all of
which send CSI 1;3A for Option+Up, which the input parser reads as
KeyUp + Alt.

VS Code's integrated terminal (xterm.js on macOS) swallows plain
Option as a compose modifier by default, so Option+Up never reaches
zot as an Alt-modified arrow. Option+Shift+Up does work there:
xterm.js emits CSI 1;4A (Shift+Alt), which the parser already
accepts as alt=true. The binding has always worked in VS Code;
only the displayed hint was misleading.

Fix: a small slideBackChordHint() helper that returns
"Option+Shift+up" when TERM_PROGRAM=vscode and "Option+up"
otherwise. interactive.go's queue-hint row calls it instead of
hardcoding the chord. The binding itself is unchanged -- both
chords work on every terminal -- the hint just adapts to what the
host actually delivers.

README.md gains a one-sentence note under Queued messages
documenting both chords and that the hint adapts via
$TERM_PROGRAM.

Tests cover the VS Code branch, case-insensitive detection
(VSCode / VSCODE / VsCode), and the default for "", "ghostty",
"iTerm.app", "Apple_Terminal", "alacritty", "kitty".
commit
467ad0a9908c7d59109609daacec2f723f5ffe06
author
patriceckhart · · 2026-05-16 10:01 UTC
parent
b11e6ed
signature
unsigned
4 files · +85 -1
M README.md +2 -0
@@ -499,6 +499,8 @@ Frames containing images are full-repainted (no differential diff) to prevent st
499499
500500 You can keep typing while the agent is working. Pressing `enter` during a turn queues the message instead of interrupting: it shows up above the status bar as `sliding in: <text>` and is delivered as the next user turn the moment the current one finishes. Queue as many as you want; they run in order. `esc` cancels the active turn and drops the queue so a runaway turn doesn't flood you with stale follow-ups; `ctrl+c` while busy arms the exit hint instead of interrupting, a second `ctrl+c` within two seconds exits zot.
501501
502+To recover the most recently queued message back into the editor (to tweak it before it runs), press `Option+↑`. In VS Code's integrated terminal that chord doesn't survive xterm.js's macOS key handling — use `Option+Shift+↑` there. zot's hint line under the sliding-in queue adapts automatically based on `$TERM_PROGRAM`.
503+
502504 Slash commands also work while the agent is busy. Read-only ones (`/help`, `/jump`, `/btw`, `/sessions`, `/skills`, `/jail`, `/unjail`, `/exit`) take effect immediately. Destructive ones (`/clear`, `/compact`, `/login`, `/logout`, `/model`, `/reload-ext`) cancel the active turn first and then run.
503505
504506
@@ -499,6 +499,8 @@ Frames containing images are full-repainted (no differential diff) to prevent st
499 499
500 You can keep typing while the agent is working. Pressing `enter` during a turn queues the message instead of interrupting: it shows up above the status bar as `sliding in: <text>` and is delivered as the next user turn the moment the current one finishes. Queue as many as you want; they run in order. `esc` cancels the active turn and drops the queue so a runaway turn doesn't flood you with stale follow-ups; `ctrl+c` while busy arms the exit hint instead of interrupting, a second `ctrl+c` within two seconds exits zot.500 You can keep typing while the agent is working. Pressing `enter` during a turn queues the message instead of interrupting: it shows up above the status bar as `sliding in: <text>` and is delivered as the next user turn the moment the current one finishes. Queue as many as you want; they run in order. `esc` cancels the active turn and drops the queue so a runaway turn doesn't flood you with stale follow-ups; `ctrl+c` while busy arms the exit hint instead of interrupting, a second `ctrl+c` within two seconds exits zot.
501 501
502+To recover the most recently queued message back into the editor (to tweak it before it runs), press `Option+↑`. In VS Code's integrated terminal that chord doesn't survive xterm.js's macOS key handling — use `Option+Shift+↑` there. zot's hint line under the sliding-in queue adapts automatically based on `$TERM_PROGRAM`.
503+
502 Slash commands also work while the agent is busy. Read-only ones (`/help`, `/jump`, `/btw`, `/sessions`, `/skills`, `/jail`, `/unjail`, `/exit`) take effect immediately. Destructive ones (`/clear`, `/compact`, `/login`, `/logout`, `/model`, `/reload-ext`) cancel the active turn first and then run.504 Slash commands also work while the agent is busy. Read-only ones (`/help`, `/jump`, `/btw`, `/sessions`, `/skills`, `/jail`, `/unjail`, `/exit`) take effect immediately. Destructive ones (`/clear`, `/compact`, `/login`, `/logout`, `/model`, `/reload-ext`) cancel the active turn first and then run.
503 505
504 506
M internal/agent/modes/interactive.go +1 -1
@@ -1030,7 +1030,7 @@ func (i *Interactive) redraw() {
10301030 // info on the status bar so it reads as ambient metadata
10311031 // rather than a chip. Tells the user how to recover the
10321032 // most recent queued message back into the editor.
1033- hint := " Press Option+↑ to slide back into input"
1033+ hint := " Press " + slideBackChordHint() + " to slide back into input"
10341034 queue = append(queue, i.cfg.Theme.FG256(i.cfg.Theme.Muted, hint))
10351035 }
10361036
@@ -1030,7 +1030,7 @@ func (i *Interactive) redraw() {
1030 // info on the status bar so it reads as ambient metadata1030 // info on the status bar so it reads as ambient metadata
1031 // rather than a chip. Tells the user how to recover the1031 // rather than a chip. Tells the user how to recover the
1032 // most recent queued message back into the editor.1032 // most recent queued message back into the editor.
1033- hint := " Press Option+↑ to slide back into input"1033+ hint := " Press " + slideBackChordHint() + " to slide back into input"
1034 queue = append(queue, i.cfg.Theme.FG256(i.cfg.Theme.Muted, hint))1034 queue = append(queue, i.cfg.Theme.FG256(i.cfg.Theme.Muted, hint))
1035 }1035 }
1036 1036
A internal/agent/modes/keychord_hint.go +39 -0
@@ -0,0 +1,39 @@
1+package modes
2+
3+import (
4+ "os"
5+ "strings"
6+)
7+
8+// slideBackChordHint returns the keyboard chord the user must press
9+// to peel the most recently queued ("sliding in") message back into
10+// the editor. The chord itself is decoded the same way everywhere
11+// (see KeyUp with k.Alt in handleKey) — what changes is which
12+// physical combo actually delivers a modified-arrow escape sequence
13+// to the process:
14+//
15+// - Ghostty, iTerm2 (with the "Esc+" / Meta=Option setting),
16+// Terminal.app (with "Use Option as Meta key"), Alacritty,
17+// Kitty: bare Option+Up sends \x1b[1;3A (mod=3 / Alt) which
18+// our parser reads as KeyUp + Alt. Good.
19+//
20+// - VS Code's integrated terminal (xterm.js) on macOS does NOT
21+// forward Option+Up as an Alt-modified arrow by default — it
22+// either swallows the Option as a compose modifier or emits a
23+// character. But Option+Shift+Up reliably sends \x1b[1;4A
24+// (mod=4 / Shift+Alt) because the Shift bit forces the
25+// CSI-with-modifier path. Our parser already treats mod=4 as
26+// alt=true, so the binding fires; we just need to tell the
27+// user the right chord.
28+//
29+// Detection is by $TERM_PROGRAM; VS Code sets this to "vscode" on
30+// every platform it ships an integrated terminal on. The decoder
31+// accepts both chords on any terminal, so getting the env-var sniff
32+// wrong only ever shows a slightly-off hint string — never breaks
33+// the binding itself.
34+func slideBackChordHint() string {
35+ if strings.EqualFold(os.Getenv("TERM_PROGRAM"), "vscode") {
36+ return "Option+Shift+↑"
37+ }
38+ return "Option+↑"
39+}
@@ -0,0 +1,39 @@
1+package modes
2+
3+import (
4+ "os"
5+ "strings"
6+)
7+
8+// slideBackChordHint returns the keyboard chord the user must press
9+// to peel the most recently queued ("sliding in") message back into
10+// the editor. The chord itself is decoded the same way everywhere
11+// (see KeyUp with k.Alt in handleKey) — what changes is which
12+// physical combo actually delivers a modified-arrow escape sequence
13+// to the process:
14+//
15+// - Ghostty, iTerm2 (with the "Esc+" / Meta=Option setting),
16+// Terminal.app (with "Use Option as Meta key"), Alacritty,
17+// Kitty: bare Option+Up sends \x1b[1;3A (mod=3 / Alt) which
18+// our parser reads as KeyUp + Alt. Good.
19+//
20+// - VS Code's integrated terminal (xterm.js) on macOS does NOT
21+// forward Option+Up as an Alt-modified arrow by default — it
22+// either swallows the Option as a compose modifier or emits a
23+// character. But Option+Shift+Up reliably sends \x1b[1;4A
24+// (mod=4 / Shift+Alt) because the Shift bit forces the
25+// CSI-with-modifier path. Our parser already treats mod=4 as
26+// alt=true, so the binding fires; we just need to tell the
27+// user the right chord.
28+//
29+// Detection is by $TERM_PROGRAM; VS Code sets this to "vscode" on
30+// every platform it ships an integrated terminal on. The decoder
31+// accepts both chords on any terminal, so getting the env-var sniff
32+// wrong only ever shows a slightly-off hint string — never breaks
33+// the binding itself.
34+func slideBackChordHint() string {
35+ if strings.EqualFold(os.Getenv("TERM_PROGRAM"), "vscode") {
36+ return "Option+Shift+↑"
37+ }
38+ return "Option+↑"
39+}
A internal/agent/modes/keychord_hint_test.go +43 -0
@@ -0,0 +1,43 @@
1+package modes
2+
3+import "testing"
4+
5+// TestSlideBackChordHintVSCode pins the VS Code branch: when the
6+// process is running under VS Code's integrated terminal the hint
7+// must direct the user to Option+Shift+↑, because plain Option+↑
8+// gets swallowed by xterm.js's default macOS key handling.
9+func TestSlideBackChordHintVSCode(t *testing.T) {
10+ t.Setenv("TERM_PROGRAM", "vscode")
11+ if got, want := slideBackChordHint(), "Option+Shift+↑"; got != want {
12+ t.Errorf("VS Code hint = %q; want %q", got, want)
13+ }
14+}
15+
16+// TestSlideBackChordHintVSCodeCaseInsensitive guards against a
17+// future VS Code change that capitalises the env-var value
18+// ("VSCode" / "VSCODE"). EqualFold keeps the detection robust.
19+func TestSlideBackChordHintVSCodeCaseInsensitive(t *testing.T) {
20+ for _, v := range []string{"VSCode", "VSCODE", "VsCode"} {
21+ t.Run(v, func(t *testing.T) {
22+ t.Setenv("TERM_PROGRAM", v)
23+ if got, want := slideBackChordHint(), "Option+Shift+↑"; got != want {
24+ t.Errorf("TERM_PROGRAM=%q hint = %q; want %q", v, got, want)
25+ }
26+ })
27+ }
28+}
29+
30+// TestSlideBackChordHintDefault pins the default for every
31+// non-VS-Code terminal: the snappier Option+↑ chord, which Ghostty,
32+// iTerm2 (with Meta=Option), Terminal.app (with Use Option as Meta),
33+// Alacritty and Kitty all deliver natively.
34+func TestSlideBackChordHintDefault(t *testing.T) {
35+ for _, v := range []string{"", "ghostty", "iTerm.app", "Apple_Terminal", "alacritty", "kitty"} {
36+ t.Run(v, func(t *testing.T) {
37+ t.Setenv("TERM_PROGRAM", v)
38+ if got, want := slideBackChordHint(), "Option+↑"; got != want {
39+ t.Errorf("TERM_PROGRAM=%q hint = %q; want %q", v, got, want)
40+ }
41+ })
42+ }
43+}
@@ -0,0 +1,43 @@
1+package modes
2+
3+import "testing"
4+
5+// TestSlideBackChordHintVSCode pins the VS Code branch: when the
6+// process is running under VS Code's integrated terminal the hint
7+// must direct the user to Option+Shift+↑, because plain Option+↑
8+// gets swallowed by xterm.js's default macOS key handling.
9+func TestSlideBackChordHintVSCode(t *testing.T) {
10+ t.Setenv("TERM_PROGRAM", "vscode")
11+ if got, want := slideBackChordHint(), "Option+Shift+↑"; got != want {
12+ t.Errorf("VS Code hint = %q; want %q", got, want)
13+ }
14+}
15+
16+// TestSlideBackChordHintVSCodeCaseInsensitive guards against a
17+// future VS Code change that capitalises the env-var value
18+// ("VSCode" / "VSCODE"). EqualFold keeps the detection robust.
19+func TestSlideBackChordHintVSCodeCaseInsensitive(t *testing.T) {
20+ for _, v := range []string{"VSCode", "VSCODE", "VsCode"} {
21+ t.Run(v, func(t *testing.T) {
22+ t.Setenv("TERM_PROGRAM", v)
23+ if got, want := slideBackChordHint(), "Option+Shift+↑"; got != want {
24+ t.Errorf("TERM_PROGRAM=%q hint = %q; want %q", v, got, want)
25+ }
26+ })
27+ }
28+}
29+
30+// TestSlideBackChordHintDefault pins the default for every
31+// non-VS-Code terminal: the snappier Option+↑ chord, which Ghostty,
32+// iTerm2 (with Meta=Option), Terminal.app (with Use Option as Meta),
33+// Alacritty and Kitty all deliver natively.
34+func TestSlideBackChordHintDefault(t *testing.T) {
35+ for _, v := range []string{"", "ghostty", "iTerm.app", "Apple_Terminal", "alacritty", "kitty"} {
36+ t.Run(v, func(t *testing.T) {
37+ t.Setenv("TERM_PROGRAM", v)
38+ if got, want := slideBackChordHint(), "Option+↑"; got != want {
39+ t.Errorf("TERM_PROGRAM=%q hint = %q; want %q", v, got, want)
40+ }
41+ })
42+ }
43+}

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 1

Tags 477

↑↓ moveenter openesc close