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 { | |||
| 44 | 44 | // the same name. | |
| 45 | 45 | 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 | 52 | ListModels bool | |
| 48 | 53 | Help bool | |
| 49 | 54 | Version bool | |
| @@ -135,6 +140,8 @@ func ParseArgs(in []string) (Args, error) { | |||
| 135 | 140 | // extension.json. Resolved to absolute later so paths like | |
| 136 | 141 | // "." survive a later cwd change. | |
| 137 | 142 | a.Exts = append(a.Exts, v) | |
| 143 | + | case "--no-ext", "--no-extensions": | |
| 144 | + | a.NoExt = true | |
| 138 | 145 | case "--reasoning": | |
| 139 | 146 | v, err := want(&i, arg) | |
| 140 | 147 | if err != nil { | |
| @@ -237,6 +244,9 @@ flags: | |||
| 237 | 244 | -e, --ext PATH load an extension from PATH for this run | |
| 238 | 245 | (repeatable; takes precedence over | |
| 239 | 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 | 251 | --max-steps N agent loop iteration cap (default 50) | |
| 242 | 252 | --list-models print known models and exit | |
| @@ -44,6 +44,11 @@ type Args struct { | |||||
| 44 | // the same name. | 44 | // the same name. | ||
| 45 | Exts []string | 45 | 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 bool | 52 | ListModels bool | ||
| 48 | Help bool | 53 | Help bool | ||
| 49 | Version bool | 54 | Version bool | ||
| @@ -135,6 +140,8 @@ func ParseArgs(in []string) (Args, error) { | |||||
| 135 | // extension.json. Resolved to absolute later so paths like | 140 | // 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 run | 244 | -e, --ext PATH load an extension from PATH for this run | ||
| 238 | (repeatable; takes precedence over | 245 | (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 exit | 252 | --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 { | |||
| 259 | 259 | for _, e := range extMgr.LoadExplicit(ctx, args.Exts) { | |
| 260 | 260 | fmt.Fprintln(os.Stderr, "extension load:", e) | |
| 261 | 261 | } | |
| 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 | + | } | |
| 265 | 269 | } | |
| 266 | 270 | // Wait briefly for extensions to flush their initial register_tool | |
| 267 | 271 | // 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_tool | 270 | // Wait briefly for extensions to flush their initial register_tool | ||
| 267 | // frames before we build the agent's tool registry. Half a second | 271 | // 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 | |||
| 317 | 317 | var lines []string | |
| 318 | 318 | for i, c := range m { | |
| 319 | 319 | 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. | |
| 324 | 322 | lines = append(lines, "") | |
| 325 | 323 | rule := strings.Repeat("─", width) | |
| 326 | 324 | label := "── " + c.Name + " " | |
| @@ -328,6 +326,7 @@ func (s *slashSuggester) Render(input string, th tui.Theme, width int) []string | |||
| 328 | 326 | rule = label + strings.Repeat("─", width-len(label)) | |
| 329 | 327 | } | |
| 330 | 328 | lines = append(lines, th.FG256(th.Muted, rule)) | |
| 329 | + | lines = append(lines, "") | |
| 331 | 330 | continue | |
| 332 | 331 | } | |
| 333 | 332 | name := c.Name | |
| @@ -341,9 +340,12 @@ func (s *slashSuggester) Render(input string, th tui.Theme, width int) []string | |||
| 341 | 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 | 346 | 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. | |
| 347 | 349 | lines = append(lines, "") | |
| 348 | 350 | return lines | |
| 349 | 351 | } | |
| @@ -317,10 +317,8 @@ func (s *slashSuggester) Render(input string, th tui.Theme, width int) []string | |||||
| 317 | var lines []string | 317 | 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 row | 320 | + | // Breathing room around group dividers — a blank row |
| 321 | - | // makes the boundary read at a glance, and the popup | 321 | + | // 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 | continue | 330 | continue | ||
| 332 | } | 331 | } | ||
| 333 | name := c.Name | 332 | 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 / editor | 347 | + | // 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 lines | 350 | 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 { | |||
| 53 | 53 | for _, e := range extMgr.LoadExplicit(ctx, args.Exts) { | |
| 54 | 54 | fmt.Fprintln(os.Stderr, "extension load:", e) | |
| 55 | 55 | } | |
| 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 | + | } | |
| 59 | 60 | } | |
| 60 | 61 | extMgr.WaitForReady(3 * time.Second) | |
| 61 | 62 | 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) | ||