Hide unjail command unless jailed

This commit is contained in:
patriceckhart 2026-05-07 18:41:13 +02:00
parent 380eca9615
commit 528ecf2db3
3 changed files with 65 additions and 4 deletions

View file

@ -912,6 +912,7 @@ func (i *Interactive) redraw() {
// when the editor starts with "/" and no dialog is already open.
// Feed extension-registered commands into the suggester first so
// they show up in tab-complete + the popup alongside the built-ins.
i.suggest.SetJailed(i.cfg.Sandbox.Locked())
if i.cfg.Extensions != nil {
catalog := i.cfg.Extensions.Commands()
extra := make([]slashCommand, 0, len(catalog))

View file

@ -57,6 +57,10 @@ var slashCatalog = []slashCommand{
type slashSuggester struct {
cursor int
// jailed tracks whether the sandbox is currently locked. It is used
// to hide state-dependent commands from the autocomplete popup.
jailed bool
// extra are commands contributed by extensions, refreshed each
// frame from the extension manager. Empty when no extensions
// have registered any. Sorted by name in SetExtra so map
@ -80,20 +84,25 @@ func (s *slashSuggester) SetExtra(cmds []slashCommand) {
s.extra = sorted
}
// SetJailed updates the current sandbox state. Called once per render
// so state-dependent commands can appear/disappear immediately.
func (s *slashSuggester) SetJailed(jailed bool) { s.jailed = jailed }
// allCatalog returns slashCatalog plus the current extra commands
// (extension-registered) with a header divider between the two
// groups. Extra entries are only kept if they don't collide with
// a built-in name; the built-in always wins.
func (s *slashSuggester) allCatalog() []slashCommand {
base := s.baseCatalog()
if len(s.extra) == 0 {
return slashCatalog
return base
}
out := make([]slashCommand, 0, len(slashCatalog)+len(s.extra)+1)
out = append(out, slashCatalog...)
out := make([]slashCommand, 0, len(base)+len(s.extra)+1)
out = append(out, base...)
var kept []slashCommand
for _, c := range s.extra {
dup := false
for _, b := range slashCatalog {
for _, b := range base {
if b.Name == c.Name {
dup = true
break
@ -110,6 +119,22 @@ func (s *slashSuggester) allCatalog() []slashCommand {
return out
}
// baseCatalog returns the built-in commands visible for the current
// interactive state.
func (s *slashSuggester) baseCatalog() []slashCommand {
if s.jailed {
return slashCatalog
}
out := make([]slashCommand, 0, len(slashCatalog)-1)
for _, c := range slashCatalog {
if c.Name == "/unjail" {
continue
}
out = append(out, c)
}
return out
}
// looksLikeSlashCommand reports whether text is an attempt at a slash
// command (valid or not). Returns true for things like "/foo" or
// "/bar baz" but false for paths ("/Users/pat/...") and regexes

View file

@ -0,0 +1,35 @@
package modes
import "testing"
func TestSlashSuggesterHidesUnjailUntilJailed(t *testing.T) {
s := newSlashSuggester()
if got := commandNames(s.matches("/unj")); contains(got, "/unjail") {
t.Fatalf("/unjail should be hidden while not jailed, got %v", got)
}
s.SetJailed(true)
if got := commandNames(s.matches("/unj")); !contains(got, "/unjail") {
t.Fatalf("/unjail should be visible while jailed, got %v", got)
}
}
func commandNames(cmds []slashCommand) []string {
out := make([]string, 0, len(cmds))
for _, c := range cmds {
if !c.Header {
out = append(out, c.Name)
}
}
return out
}
func contains(xs []string, want string) bool {
for _, x := range xs {
if x == want {
return true
}
}
return false
}