detached 84dbd86

feat(extensions): --no-ext to skip discovery for one run

Useful for clean-room runs ("does this prompt work without my
extensions interfering?") and for reproducing issues without
auto-loaded guardrails.

  zot --no-ext              # zero extensions
  zot --no-ext --ext ./x    # only x; nothing else gets discovered

Explicit --ext paths still load on top, so --no-ext is "skip the
implicit scan", not "block all extensions". Lets you run with
exactly one extension without uninstalling the rest.

Wired into both interactive (cli.go) and rpc (rpc.go) modes.
Help text updated.

The "── extensions ────" divider in the slash autocomplete
already hides itself when no extension commands are registered
(empty s.extra short-circuits allCatalog before the header
insertion path), so --no-ext naturally produces a clean popup
with no orphan rule. pruneOrphanHeaders also drops the rule
when filter input narrows the extension group to zero matches.

End-to-end verified live:
  default                  : agent sees weather + read_notes
  --no-ext                 : agent sees only the 5 built-ins
  --no-ext --ext scratchpad: agent sees built-ins + read_notes
commit
84dbd86aee6e978a9600716c46022e21cee59884
author
patriceckhart · · 2026-04-19 13:42 UTC
parent
e9b466a
signature
unsigned
refs
tag: v0.0.28
4 files · +29 -12
M internal/agent/args.go +10 -0
@@ -44,6 +44,11 @@ type Args struct {
4444 // the same name.
4545 Exts []string
4646
47+ // NoExt disables extension discovery + spawn entirely for this
48+ // run. --ext PATH still works (explicit beats implicit) so you
49+ // can run "with only this one extension" via --no-ext --ext PATH.
50+ NoExt bool
51+
4752 ListModels bool
4853 Help bool
4954 Version bool
@@ -135,6 +140,8 @@ func ParseArgs(in []string) (Args, error) {
135140 // extension.json. Resolved to absolute later so paths like
136141 // "." survive a later cwd change.
137142 a.Exts = append(a.Exts, v)
143+ case "--no-ext", "--no-extensions":
144+ a.NoExt = true
138145 case "--reasoning":
139146 v, err := want(&i, arg)
140147 if err != nil {
@@ -237,6 +244,9 @@ flags:
237244 -e, --ext PATH load an extension from PATH for this run
238245 (repeatable; takes precedence over
239246 installed extensions of the same name)
247+ --no-ext skip extension discovery for this run
248+ (--ext PATH still works on top, so
249+ --no-ext --ext ./x runs only x)
240250
241251 --max-steps N agent loop iteration cap (default 50)
242252 --list-models print known models and exit
@@ -44,6 +44,11 @@ type Args struct {
44 // the same name.44 // the same name.
45 Exts []string45 Exts []string
46 46
47+ // NoExt disables extension discovery + spawn entirely for this
48+ // run. --ext PATH still works (explicit beats implicit) so you
49+ // can run "with only this one extension" via --no-ext --ext PATH.
50+ NoExt bool
51+
47 ListModels bool52 ListModels bool
48 Help bool53 Help bool
49 Version bool54 Version bool
@@ -135,6 +140,8 @@ func ParseArgs(in []string) (Args, error) {
135 // extension.json. Resolved to absolute later so paths like140 // extension.json. Resolved to absolute later so paths like
136 // "." survive a later cwd change.141 // "." survive a later cwd change.
137 a.Exts = append(a.Exts, v)142 a.Exts = append(a.Exts, v)
143+ case "--no-ext", "--no-extensions":
144+ a.NoExt = true
138 case "--reasoning":145 case "--reasoning":
139 v, err := want(&i, arg)146 v, err := want(&i, arg)
140 if err != nil {147 if err != nil {
@@ -237,6 +244,9 @@ flags:
237 -e, --ext PATH load an extension from PATH for this run244 -e, --ext PATH load an extension from PATH for this run
238 (repeatable; takes precedence over245 (repeatable; takes precedence over
239 installed extensions of the same name)246 installed extensions of the same name)
247+ --no-ext skip extension discovery for this run
248+ (--ext PATH still works on top, so
249+ --no-ext --ext ./x runs only x)
240 250
241 --max-steps N agent loop iteration cap (default 50)251 --max-steps N agent loop iteration cap (default 50)
242 --list-models print known models and exit252 --list-models print known models and exit
M internal/agent/cli.go +7 -3
@@ -259,9 +259,13 @@ func runInteractive(ctx context.Context, args Args, version string) error {
259259 for _, e := range extMgr.LoadExplicit(ctx, args.Exts) {
260260 fmt.Fprintln(os.Stderr, "extension load:", e)
261261 }
262- discoveryErrs := extMgr.Discover(ctx)
263- for _, e := range discoveryErrs {
264- fmt.Fprintln(os.Stderr, "extension load:", e)
262+ // --no-ext skips the global + project-local discovery scan;
263+ // explicit --ext paths above are still honoured so you can run
264+ // "only this extension" with --no-ext --ext ./x.
265+ if !args.NoExt {
266+ for _, e := range extMgr.Discover(ctx) {
267+ fmt.Fprintln(os.Stderr, "extension load:", e)
268+ }
265269 }
266270 // Wait briefly for extensions to flush their initial register_tool
267271 // frames before we build the agent's tool registry. Half a second
@@ -259,9 +259,13 @@ func runInteractive(ctx context.Context, args Args, version string) error {
259 for _, e := range extMgr.LoadExplicit(ctx, args.Exts) {259 for _, e := range extMgr.LoadExplicit(ctx, args.Exts) {
260 fmt.Fprintln(os.Stderr, "extension load:", e)260 fmt.Fprintln(os.Stderr, "extension load:", e)
261 }261 }
262- discoveryErrs := extMgr.Discover(ctx)262+ // --no-ext skips the global + project-local discovery scan;
263- for _, e := range discoveryErrs {263+ // explicit --ext paths above are still honoured so you can run
264- fmt.Fprintln(os.Stderr, "extension load:", e)264+ // "only this extension" with --no-ext --ext ./x.
265+ if !args.NoExt {
266+ for _, e := range extMgr.Discover(ctx) {
267+ fmt.Fprintln(os.Stderr, "extension load:", e)
268+ }
265 }269 }
266 // Wait briefly for extensions to flush their initial register_tool270 // Wait briefly for extensions to flush their initial register_tool
267 // frames before we build the agent's tool registry. Half a second271 // frames before we build the agent's tool registry. Half a second
M internal/agent/modes/slash_suggest.go +8 -6
@@ -317,10 +317,8 @@ func (s *slashSuggester) Render(input string, th tui.Theme, width int) []string
317317 var lines []string
318318 for i, c := range m {
319319 if c.Header {
320- // Breathing room before group dividers — a blank row
321- // makes the boundary read at a glance, and the popup
322- // only ever has at most one or two groups so the cost
323- // is negligible.
320+ // Breathing room around group dividers — a blank row
321+ // before AND after makes the boundary read at a glance.
324322 lines = append(lines, "")
325323 rule := strings.Repeat("─", width)
326324 label := "── " + c.Name + " "
@@ -328,6 +326,7 @@ func (s *slashSuggester) Render(input string, th tui.Theme, width int) []string
328326 rule = label + strings.Repeat("─", width-len(label))
329327 }
330328 lines = append(lines, th.FG256(th.Muted, rule))
329+ lines = append(lines, "")
331330 continue
332331 }
333332 name := c.Name
@@ -341,9 +340,12 @@ func (s *slashSuggester) Render(input string, th tui.Theme, width int) []string
341340 lines = append(lines, th.FG256(th.Muted, plain))
342341 }
343342 }
343+ // Blank row before the hint visually detaches it from the
344+ // command list and groups it with its trailing blank.
345+ lines = append(lines, "")
344346 lines = append(lines, th.FG256(th.Muted, " ↑/↓ navigate · tab complete · enter run"))
345- // Blank row separates the popup from the status bar / editor
346- // below it so the hint line doesn't visually crash into them.
347+ // Blank row after the hint separates the popup from the status
348+ // bar / editor below it.
347349 lines = append(lines, "")
348350 return lines
349351 }
@@ -317,10 +317,8 @@ func (s *slashSuggester) Render(input string, th tui.Theme, width int) []string
317 var lines []string317 var lines []string
318 for i, c := range m {318 for i, c := range m {
319 if c.Header {319 if c.Header {
320- // Breathing room before group dividers — a blank row320+ // Breathing room around group dividers — a blank row
321- // makes the boundary read at a glance, and the popup321+ // before AND after makes the boundary read at a glance.
322- // only ever has at most one or two groups so the cost
323- // is negligible.
324 lines = append(lines, "")322 lines = append(lines, "")
325 rule := strings.Repeat("─", width)323 rule := strings.Repeat("─", width)
326 label := "── " + c.Name + " "324 label := "── " + c.Name + " "
@@ -328,6 +326,7 @@ func (s *slashSuggester) Render(input string, th tui.Theme, width int) []string
328 rule = label + strings.Repeat("─", width-len(label))326 rule = label + strings.Repeat("─", width-len(label))
329 }327 }
330 lines = append(lines, th.FG256(th.Muted, rule))328 lines = append(lines, th.FG256(th.Muted, rule))
329+ lines = append(lines, "")
331 continue330 continue
332 }331 }
333 name := c.Name332 name := c.Name
@@ -341,9 +340,12 @@ func (s *slashSuggester) Render(input string, th tui.Theme, width int) []string
341 lines = append(lines, th.FG256(th.Muted, plain))340 lines = append(lines, th.FG256(th.Muted, plain))
342 }341 }
343 }342 }
343+ // Blank row before the hint visually detaches it from the
344+ // command list and groups it with its trailing blank.
345+ lines = append(lines, "")
344 lines = append(lines, th.FG256(th.Muted, " ↑/↓ navigate · tab complete · enter run"))346 lines = append(lines, th.FG256(th.Muted, " ↑/↓ navigate · tab complete · enter run"))
345- // Blank row separates the popup from the status bar / editor347+ // Blank row after the hint separates the popup from the status
346- // below it so the hint line doesn't visually crash into them.348+ // bar / editor below it.
347 lines = append(lines, "")349 lines = append(lines, "")
348 return lines350 return lines
349 }351 }
M internal/agent/rpc.go +4 -3
@@ -53,9 +53,10 @@ func runRPCMode(ctx context.Context, args Args, version string) error {
5353 for _, e := range extMgr.LoadExplicit(ctx, args.Exts) {
5454 fmt.Fprintln(os.Stderr, "extension load:", e)
5555 }
56- discoveryErrs := extMgr.Discover(ctx)
57- for _, e := range discoveryErrs {
58- fmt.Fprintln(os.Stderr, "extension load:", e)
56+ if !args.NoExt {
57+ for _, e := range extMgr.Discover(ctx) {
58+ fmt.Fprintln(os.Stderr, "extension load:", e)
59+ }
5960 }
6061 extMgr.WaitForReady(3 * time.Second)
6162 defer extMgr.Stop(2 * time.Second)
@@ -53,9 +53,10 @@ func runRPCMode(ctx context.Context, args Args, version string) error {
53 for _, e := range extMgr.LoadExplicit(ctx, args.Exts) {53 for _, e := range extMgr.LoadExplicit(ctx, args.Exts) {
54 fmt.Fprintln(os.Stderr, "extension load:", e)54 fmt.Fprintln(os.Stderr, "extension load:", e)
55 }55 }
56- discoveryErrs := extMgr.Discover(ctx)56+ if !args.NoExt {
57- for _, e := range discoveryErrs {57+ for _, e := range extMgr.Discover(ctx) {
58- fmt.Fprintln(os.Stderr, "extension load:", e)58+ fmt.Fprintln(os.Stderr, "extension load:", e)
59+ }
59 }60 }
60 extMgr.WaitForReady(3 * time.Second)61 extMgr.WaitForReady(3 * time.Second)
61 defer extMgr.Stop(2 * time.Second)62 defer extMgr.Stop(2 * time.Second)

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