detached a07e43d

interactive: blank line above slash popup and dialog block

Slash command popup and dialogs (login / model / sessions / jump /
btw / changelog / etc.) used to sit flush against the chat or
welcome content above. Add one blank row before the suggest popup
when it is non-empty and one above the dialog block when a dialog
is showing.

Cursor-row math gains a dialogLead offset so popup-side cursor
branches (btwDialog, login dialog, sessionDialog) land in the
right cell now that the dialog starts one row lower.
commit
a07e43dfd7226f49d575c7cd1e5092fb83431b10
author
patriceckhart · · 2026-04-28 17:19 UTC
parent
0cc0036
signature
unsigned
refs
tag: v0.1.41
1 file · +26 -11
M internal/agent/modes/interactive.go +26 -11
@@ -794,6 +794,11 @@ func (i *Interactive) redraw() {
794794 } else if len(dialog) == 0 && i.fileSuggest.Active(currentInput) {
795795 suggest = i.fileSuggest.Render(currentInput, i.cfg.Theme, cols)
796796 }
797+ if len(suggest) > 0 {
798+ // One blank row above the popup so it doesn't sit flush
799+ // against the chat / welcome content above.
800+ suggest = append([]string{""}, suggest...)
801+ }
797802
798803 // Busy prefix shown at the far left of the status bar. The
799804 // spinner glyph and its funny-line message share the `zot`
@@ -845,13 +850,16 @@ func (i *Interactive) redraw() {
845850 }
846851 }
847852
848- // Bottom-sticky sections (always visible, never scroll). One
849- // blank row above statusLines so the spinner / model / cwd
850- // block doesn't sit flush against the chat content above it,
851- // one blank between status bar and editor for input breathing
852- // room, and one trailing blank at the very bottom so the
853- // editor isn't flush against the terminal edge.
854- bottom := make([]string, 0, len(dialog)+len(suggest)+len(queue)+len(edLines)+4)
853+ // Bottom-sticky sections (always visible, never scroll). Each
854+ // non-empty subsection (dialog, suggest popup, sliding-in queue)
855+ // is preceded by one blank row so it has air above the chat
856+ // content. The status block and editor get their own dedicated
857+ // blanks so spacing stays consistent whether or not a dialog or
858+ // popup is showing.
859+ bottom := make([]string, 0, len(dialog)+len(suggest)+len(queue)+len(edLines)+8)
860+ if len(dialog) > 0 {
861+ bottom = append(bottom, "")
862+ }
855863 bottom = append(bottom, dialog...)
856864 bottom = append(bottom, suggest...)
857865 bottom = append(bottom, queue...)
@@ -972,28 +980,35 @@ func (i *Interactive) redraw() {
972980 // the blinking cursor shows where the user is actually typing.
973981 // Dialogs without a cursor (model picker, /help, /login, etc.)
974982 // return -1 and the main editor keeps the cursor.
983+ // dialogLead is 1 when the bottom region prepends a blank above
984+ // the dialog block (whenever a dialog is showing) so popup-side
985+ // cursor positions still land in the right cell.
986+ dialogLead := 0
987+ if len(dialog) > 0 {
988+ dialogLead = 1
989+ }
975990 // +2 accounts for the blank row above statusLines (so the
976991 // status block has air above it) and the blank row between
977992 // statusLines and edLines (input breathing room). Without
978993 // these the rendered cursor would land on a blank instead of
979994 // inside the editor row.
980- cursorRow := len(visibleChat) + len(dialog) + len(suggest) + len(queue) + 1 + len(statusLines) + 1 + curR
995+ cursorRow := len(visibleChat) + dialogLead + len(dialog) + len(suggest) + len(queue) + 1 + len(statusLines) + 1 + curR
981996 cursorCol := curC
982997 if i.btwDialog.Active() {
983998 if r, c := i.btwDialog.CursorPos(cols); r >= 0 {
984- cursorRow = len(visibleChat) + r
999+ cursorRow = len(visibleChat) + dialogLead + r
9851000 cursorCol = c
9861001 }
9871002 }
9881003 if i.dialog.Active() {
9891004 if r, c := i.dialog.CursorPos(cols); r >= 0 {
990- cursorRow = len(visibleChat) + r
1005+ cursorRow = len(visibleChat) + dialogLead + r
9911006 cursorCol = c
9921007 }
9931008 }
9941009 if i.sessionDialog.Active() {
9951010 if r, c := i.sessionDialog.CursorPos(); r >= 0 {
996- cursorRow = len(visibleChat) + r
1011+ cursorRow = len(visibleChat) + dialogLead + r
9971012 cursorCol = c
9981013 }
9991014 }
@@ -794,6 +794,11 @@ func (i *Interactive) redraw() {
794 } else if len(dialog) == 0 && i.fileSuggest.Active(currentInput) {794 } else if len(dialog) == 0 && i.fileSuggest.Active(currentInput) {
795 suggest = i.fileSuggest.Render(currentInput, i.cfg.Theme, cols)795 suggest = i.fileSuggest.Render(currentInput, i.cfg.Theme, cols)
796 }796 }
797+ if len(suggest) > 0 {
798+ // One blank row above the popup so it doesn't sit flush
799+ // against the chat / welcome content above.
800+ suggest = append([]string{""}, suggest...)
801+ }
797 802
798 // Busy prefix shown at the far left of the status bar. The803 // Busy prefix shown at the far left of the status bar. The
799 // spinner glyph and its funny-line message share the `zot`804 // spinner glyph and its funny-line message share the `zot`
@@ -845,13 +850,16 @@ func (i *Interactive) redraw() {
845 }850 }
846 }851 }
847 852
848- // Bottom-sticky sections (always visible, never scroll). One853+ // Bottom-sticky sections (always visible, never scroll). Each
849- // blank row above statusLines so the spinner / model / cwd854+ // non-empty subsection (dialog, suggest popup, sliding-in queue)
850- // block doesn't sit flush against the chat content above it,855+ // is preceded by one blank row so it has air above the chat
851- // one blank between status bar and editor for input breathing856+ // content. The status block and editor get their own dedicated
852- // room, and one trailing blank at the very bottom so the857+ // blanks so spacing stays consistent whether or not a dialog or
853- // editor isn't flush against the terminal edge.858+ // popup is showing.
854- bottom := make([]string, 0, len(dialog)+len(suggest)+len(queue)+len(edLines)+4)859+ bottom := make([]string, 0, len(dialog)+len(suggest)+len(queue)+len(edLines)+8)
860+ if len(dialog) > 0 {
861+ bottom = append(bottom, "")
862+ }
855 bottom = append(bottom, dialog...)863 bottom = append(bottom, dialog...)
856 bottom = append(bottom, suggest...)864 bottom = append(bottom, suggest...)
857 bottom = append(bottom, queue...)865 bottom = append(bottom, queue...)
@@ -972,28 +980,35 @@ func (i *Interactive) redraw() {
972 // the blinking cursor shows where the user is actually typing.980 // the blinking cursor shows where the user is actually typing.
973 // Dialogs without a cursor (model picker, /help, /login, etc.)981 // Dialogs without a cursor (model picker, /help, /login, etc.)
974 // return -1 and the main editor keeps the cursor.982 // return -1 and the main editor keeps the cursor.
983+ // dialogLead is 1 when the bottom region prepends a blank above
984+ // the dialog block (whenever a dialog is showing) so popup-side
985+ // cursor positions still land in the right cell.
986+ dialogLead := 0
987+ if len(dialog) > 0 {
988+ dialogLead = 1
989+ }
975 // +2 accounts for the blank row above statusLines (so the990 // +2 accounts for the blank row above statusLines (so the
976 // status block has air above it) and the blank row between991 // status block has air above it) and the blank row between
977 // statusLines and edLines (input breathing room). Without992 // statusLines and edLines (input breathing room). Without
978 // these the rendered cursor would land on a blank instead of993 // these the rendered cursor would land on a blank instead of
979 // inside the editor row.994 // inside the editor row.
980- cursorRow := len(visibleChat) + len(dialog) + len(suggest) + len(queue) + 1 + len(statusLines) + 1 + curR995+ cursorRow := len(visibleChat) + dialogLead + len(dialog) + len(suggest) + len(queue) + 1 + len(statusLines) + 1 + curR
981 cursorCol := curC996 cursorCol := curC
982 if i.btwDialog.Active() {997 if i.btwDialog.Active() {
983 if r, c := i.btwDialog.CursorPos(cols); r >= 0 {998 if r, c := i.btwDialog.CursorPos(cols); r >= 0 {
984- cursorRow = len(visibleChat) + r999+ cursorRow = len(visibleChat) + dialogLead + r
985 cursorCol = c1000 cursorCol = c
986 }1001 }
987 }1002 }
988 if i.dialog.Active() {1003 if i.dialog.Active() {
989 if r, c := i.dialog.CursorPos(cols); r >= 0 {1004 if r, c := i.dialog.CursorPos(cols); r >= 0 {
990- cursorRow = len(visibleChat) + r1005+ cursorRow = len(visibleChat) + dialogLead + r
991 cursorCol = c1006 cursorCol = c
992 }1007 }
993 }1008 }
994 if i.sessionDialog.Active() {1009 if i.sessionDialog.Active() {
995 if r, c := i.sessionDialog.CursorPos(); r >= 0 {1010 if r, c := i.sessionDialog.CursorPos(); r >= 0 {
996- cursorRow = len(visibleChat) + r1011+ cursorRow = len(visibleChat) + dialogLead + r
997 cursorCol = c1012 cursorCol = c
998 }1013 }
999 }1014 }

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 476

↑↓ moveenter openesc close