detached 528ecf2

Hide unjail command unless jailed

commit
528ecf2db3808a8acbbf71eacd5833be9ceba0da
author
patriceckhart · · 2026-05-07 16:41 UTC
parent
380eca9
signature
unsigned
refs
tag: v0.1.63
3 files · +65 -4
M internal/agent/modes/interactive.go +1 -0
@@ -912,6 +912,7 @@ func (i *Interactive) redraw() {
912912 // when the editor starts with "/" and no dialog is already open.
913913 // Feed extension-registered commands into the suggester first so
914914 // they show up in tab-complete + the popup alongside the built-ins.
915+ i.suggest.SetJailed(i.cfg.Sandbox.Locked())
915916 if i.cfg.Extensions != nil {
916917 catalog := i.cfg.Extensions.Commands()
917918 extra := make([]slashCommand, 0, len(catalog))
@@ -912,6 +912,7 @@ func (i *Interactive) redraw() {
912 // when the editor starts with "/" and no dialog is already open.912 // when the editor starts with "/" and no dialog is already open.
913 // Feed extension-registered commands into the suggester first so913 // Feed extension-registered commands into the suggester first so
914 // they show up in tab-complete + the popup alongside the built-ins.914 // they show up in tab-complete + the popup alongside the built-ins.
915+ i.suggest.SetJailed(i.cfg.Sandbox.Locked())
915 if i.cfg.Extensions != nil {916 if i.cfg.Extensions != nil {
916 catalog := i.cfg.Extensions.Commands()917 catalog := i.cfg.Extensions.Commands()
917 extra := make([]slashCommand, 0, len(catalog))918 extra := make([]slashCommand, 0, len(catalog))
M internal/agent/modes/slash_suggest.go +29 -4
@@ -57,6 +57,10 @@ var slashCatalog = []slashCommand{
5757 type slashSuggester struct {
5858 cursor int
5959
60+ // jailed tracks whether the sandbox is currently locked. It is used
61+ // to hide state-dependent commands from the autocomplete popup.
62+ jailed bool
63+
6064 // extra are commands contributed by extensions, refreshed each
6165 // frame from the extension manager. Empty when no extensions
6266 // have registered any. Sorted by name in SetExtra so map
@@ -80,20 +84,25 @@ func (s *slashSuggester) SetExtra(cmds []slashCommand) {
8084 s.extra = sorted
8185 }
8286
87+// SetJailed updates the current sandbox state. Called once per render
88+// so state-dependent commands can appear/disappear immediately.
89+func (s *slashSuggester) SetJailed(jailed bool) { s.jailed = jailed }
90+
8391 // allCatalog returns slashCatalog plus the current extra commands
8492 // (extension-registered) with a header divider between the two
8593 // groups. Extra entries are only kept if they don't collide with
8694 // a built-in name; the built-in always wins.
8795 func (s *slashSuggester) allCatalog() []slashCommand {
96+ base := s.baseCatalog()
8897 if len(s.extra) == 0 {
89- return slashCatalog
98+ return base
9099 }
91- out := make([]slashCommand, 0, len(slashCatalog)+len(s.extra)+1)
92- out = append(out, slashCatalog...)
100+ out := make([]slashCommand, 0, len(base)+len(s.extra)+1)
101+ out = append(out, base...)
93102 var kept []slashCommand
94103 for _, c := range s.extra {
95104 dup := false
96- for _, b := range slashCatalog {
105+ for _, b := range base {
97106 if b.Name == c.Name {
98107 dup = true
99108 break
@@ -110,6 +119,22 @@ func (s *slashSuggester) allCatalog() []slashCommand {
110119 return out
111120 }
112121
122+// baseCatalog returns the built-in commands visible for the current
123+// interactive state.
124+func (s *slashSuggester) baseCatalog() []slashCommand {
125+ if s.jailed {
126+ return slashCatalog
127+ }
128+ out := make([]slashCommand, 0, len(slashCatalog)-1)
129+ for _, c := range slashCatalog {
130+ if c.Name == "/unjail" {
131+ continue
132+ }
133+ out = append(out, c)
134+ }
135+ return out
136+}
137+
113138 // looksLikeSlashCommand reports whether text is an attempt at a slash
114139 // command (valid or not). Returns true for things like "/foo" or
115140 // "/bar baz" but false for paths ("/Users/pat/...") and regexes
@@ -57,6 +57,10 @@ var slashCatalog = []slashCommand{
57 type slashSuggester struct {57 type slashSuggester struct {
58 cursor int58 cursor int
59 59
60+ // jailed tracks whether the sandbox is currently locked. It is used
61+ // to hide state-dependent commands from the autocomplete popup.
62+ jailed bool
63+
60 // extra are commands contributed by extensions, refreshed each64 // extra are commands contributed by extensions, refreshed each
61 // frame from the extension manager. Empty when no extensions65 // frame from the extension manager. Empty when no extensions
62 // have registered any. Sorted by name in SetExtra so map66 // have registered any. Sorted by name in SetExtra so map
@@ -80,20 +84,25 @@ func (s *slashSuggester) SetExtra(cmds []slashCommand) {
80 s.extra = sorted84 s.extra = sorted
81 }85 }
82 86
87+// SetJailed updates the current sandbox state. Called once per render
88+// so state-dependent commands can appear/disappear immediately.
89+func (s *slashSuggester) SetJailed(jailed bool) { s.jailed = jailed }
90+
83 // allCatalog returns slashCatalog plus the current extra commands91 // allCatalog returns slashCatalog plus the current extra commands
84 // (extension-registered) with a header divider between the two92 // (extension-registered) with a header divider between the two
85 // groups. Extra entries are only kept if they don't collide with93 // groups. Extra entries are only kept if they don't collide with
86 // a built-in name; the built-in always wins.94 // a built-in name; the built-in always wins.
87 func (s *slashSuggester) allCatalog() []slashCommand {95 func (s *slashSuggester) allCatalog() []slashCommand {
96+ base := s.baseCatalog()
88 if len(s.extra) == 0 {97 if len(s.extra) == 0 {
89- return slashCatalog98+ return base
90 }99 }
91- out := make([]slashCommand, 0, len(slashCatalog)+len(s.extra)+1)100+ out := make([]slashCommand, 0, len(base)+len(s.extra)+1)
92- out = append(out, slashCatalog...)101+ out = append(out, base...)
93 var kept []slashCommand102 var kept []slashCommand
94 for _, c := range s.extra {103 for _, c := range s.extra {
95 dup := false104 dup := false
96- for _, b := range slashCatalog {105+ for _, b := range base {
97 if b.Name == c.Name {106 if b.Name == c.Name {
98 dup = true107 dup = true
99 break108 break
@@ -110,6 +119,22 @@ func (s *slashSuggester) allCatalog() []slashCommand {
110 return out119 return out
111 }120 }
112 121
122+// baseCatalog returns the built-in commands visible for the current
123+// interactive state.
124+func (s *slashSuggester) baseCatalog() []slashCommand {
125+ if s.jailed {
126+ return slashCatalog
127+ }
128+ out := make([]slashCommand, 0, len(slashCatalog)-1)
129+ for _, c := range slashCatalog {
130+ if c.Name == "/unjail" {
131+ continue
132+ }
133+ out = append(out, c)
134+ }
135+ return out
136+}
137+
113 // looksLikeSlashCommand reports whether text is an attempt at a slash138 // looksLikeSlashCommand reports whether text is an attempt at a slash
114 // command (valid or not). Returns true for things like "/foo" or139 // command (valid or not). Returns true for things like "/foo" or
115 // "/bar baz" but false for paths ("/Users/pat/...") and regexes140 // "/bar baz" but false for paths ("/Users/pat/...") and regexes
A internal/agent/modes/slash_suggest_test.go +35 -0
@@ -0,0 +1,35 @@
1+package modes
2+
3+import "testing"
4+
5+func TestSlashSuggesterHidesUnjailUntilJailed(t *testing.T) {
6+ s := newSlashSuggester()
7+
8+ if got := commandNames(s.matches("/unj")); contains(got, "/unjail") {
9+ t.Fatalf("/unjail should be hidden while not jailed, got %v", got)
10+ }
11+
12+ s.SetJailed(true)
13+ if got := commandNames(s.matches("/unj")); !contains(got, "/unjail") {
14+ t.Fatalf("/unjail should be visible while jailed, got %v", got)
15+ }
16+}
17+
18+func commandNames(cmds []slashCommand) []string {
19+ out := make([]string, 0, len(cmds))
20+ for _, c := range cmds {
21+ if !c.Header {
22+ out = append(out, c.Name)
23+ }
24+ }
25+ return out
26+}
27+
28+func contains(xs []string, want string) bool {
29+ for _, x := range xs {
30+ if x == want {
31+ return true
32+ }
33+ }
34+ return false
35+}
@@ -0,0 +1,35 @@
1+package modes
2+
3+import "testing"
4+
5+func TestSlashSuggesterHidesUnjailUntilJailed(t *testing.T) {
6+ s := newSlashSuggester()
7+
8+ if got := commandNames(s.matches("/unj")); contains(got, "/unjail") {
9+ t.Fatalf("/unjail should be hidden while not jailed, got %v", got)
10+ }
11+
12+ s.SetJailed(true)
13+ if got := commandNames(s.matches("/unj")); !contains(got, "/unjail") {
14+ t.Fatalf("/unjail should be visible while jailed, got %v", got)
15+ }
16+}
17+
18+func commandNames(cmds []slashCommand) []string {
19+ out := make([]string, 0, len(cmds))
20+ for _, c := range cmds {
21+ if !c.Header {
22+ out = append(out, c.Name)
23+ }
24+ }
25+ return out
26+}
27+
28+func contains(xs []string, want string) bool {
29+ for _, x := range xs {
30+ if x == want {
31+ return true
32+ }
33+ }
34+ return false
35+}

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