Skip to content

feat(tui): drop builtin-tool selection screen; give web search its own step#263

Merged
initializ-mk merged 3 commits into
mainfrom
feat/tui-drop-tools-step
Jul 13, 2026
Merged

feat(tui): drop builtin-tool selection screen; give web search its own step#263
initializ-mk merged 3 commits into
mainfrom
feat/tui-drop-tools-step

Conversation

@initializ-mk

Copy link
Copy Markdown
Contributor

Summary

The forge init wizard had a bulk "Built-in Tools" selection screen (checkbox list of every builtin). But every builtin is auto-registered at runtime (builtins.RegisterAll) regardless of builtin_tools — that field only ever influenced a narrow platform-policy-deny interaction, never registration. So the selection screen was busywork.

This removes it, and gives the one builtin that genuinely needs operator input — web search — its own focused step.

Changes

  • Remove ToolsStep (the builtin multi-select). builtins.RegisterAll already wires every builtin, so interactive init now leaves builtin_tools empty → all builtins on.
  • New WebSearchStep — pick Tavily / Perplexity / skip, then enter + validate the API key. Auto-skips the key prompt when the provider's key is already in the environment. Writes WEB_SEARCH_PROVIDER + TAVILY_API_KEY/PERPLEXITY_API_KEY to the agent .env (same behavior the tools step used to bury).
  • --tools flag preserved — the non-interactive path still populates builtin_tools; the now-always-empty wizard context no longer clobbers a flag-provided value (init.go).

Notes

  • Runtime behavior is unchanged for existing agents (builtins were always all-registered).
  • web_search without a provider/key stays inert (as before) — operators can add the key later via .env.

Build + golangci-lint clean; forge-cli/internal/tui/... tests green.

Follow-up (from the same feedback, separate PR): wizard back-navigation consistency and the multi-select Enter auto-select behavior.

…n step

Every builtin tool is auto-registered at runtime (builtins.RegisterAll), so the init wizard's bulk 'select which built-ins' screen served no purpose — builtin_tools only ever affected a narrow platform-policy-deny interaction, never registration. Remove ToolsStep. The one builtin that genuinely needs operator input — web_search (provider + API key) — gets its own focused WebSearchStep (Tavily / Perplexity / skip; validates the key; auto-skips the prompt when the provider key is already in the environment). The --tools flag still populates builtin_tools for non-interactive use; the now-always-empty wizard context no longer clobbers it.
…o/NewToolsStep)

The tools-drop commit only landed the tools_step.go deletion; the
replacement WebSearchStep and the init.go rewire that references it were
never committed, so the package failed to compile (undefined
steps.ToolInfo / steps.NewToolsStep). This adds the missing pieces:

- new WebSearchStep: Tavily / Perplexity / skip, validates the key, and
  auto-adopts a provider key already present in the environment.
- init.go: drop the toolInfos build, slot NewWebSearchStep in ChannelStep's
  place, and guard opts.BuiltinTools so an empty wizard context can't clobber
  a --tools flag.

@initializ-mk initializ-mk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: drop builtin-tool selection screen; give web search its own step

Good cleanup with sound justification. I verified the load-bearing claims against main rather than taking them on faith, and they hold:

  • builtin_tools really is not a registration allowlist. The tool_filter_stage.go / tool_filter.go hits that look alarming on a grep are just category annotation + dev-tool filtering keyed on tool names in the spec — nothing treats builtin_tools as "only these," and runtime uses builtins.RegisterAll unconditionally.
  • The egress interaction is correctly preserved. init_egress.go derives allowlist domains from opts.BuiltinTools (web_search → provider-filtered api.tavily.com/api.perplexity.ai; others → DefaultToolDomains). Among the builtins the old multi-select actually offered, only web_search carries egress domains (http_request is dynamic/empty; the domain-bearing names in DefaultToolDomains are adapter tools that were never in builtins.All()). The new step's Apply recording web_search + WEB_SEARCH_PROVIDER keeps that path working — the comment in Apply explaining why is accurate and appreciated.

One real fix needed before merge (inline on init.go): the --tools clobber guard only protects the skip path. Remaining notes are minors/nits, inline and below.

Notes without an inline anchor

  • Dead defensive branch: runValidation's validateFn == nil check is unreachable (guarded at the call site). Harmless.
  • Semantics drift worth a docs glance: builtin_tools now means "wizard-recorded selection for egress/policy purposes" — empty for skip-path agents, [web_search] otherwise — while docs/security/platform-policy.md and the forge.yaml.tmpl comment date from the multi-select era. Adjacent to open issue #281 (the tools:-doesn't-register confusion), which this PR's direction actually helps clarify.

Verdict

The removal is safe (verified against the filter/egress/runtime consumers), the new step is a faithful, tighter rewrite of the old web-search sub-flow, and CI is green. Fix the merge-instead-of-overwrite in init.go (ideally with the small table test suggested inline) and this is good to go.

Comment thread forge-cli/cmd/init.go Outdated
// Only overwrite from the wizard context when it actually collected
// something (e.g. web_search). An empty context must not clobber a
// --tools flag set on the command line.
if len(ctx.BuiltinTools) > 0 {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important: this guard only protects the skip path — choosing a provider still clobbers --tools.

When the user picks a web-search provider interactively, ctx.BuiltinTools is ["web_search"] and this REPLACES a flag-provided value. forge init --tools web_search,http_request + choosing Tavily in the wizard → builtin_tools: [web_search]: the flag's other entries are dropped from the yaml and from init_egress's DefaultToolDomains derivation. The PR body's "no longer clobbers a flag-provided value" holds only when the user selects "No web search."

Fix: merge and dedupe instead of overwrite, e.g. opts.BuiltinTools = dedupe(append(opts.BuiltinTools, ctx.BuiltinTools...)). A small table test (flag+skip / flag+provider / no-flag+provider) would pin all three paths — this is plain non-TUI code, so it's cheap to cover.

return s.provider
}

func (s *WebSearchStep) Apply(ctx *tui.WizardContext) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Apply is a pure function with three meaningful behaviors (skip writes nothing; provider writes WEB_SEARCH_PROVIDER + key; env-key path writes provider but no key) — worth a 10-line test even though the steps package is otherwise test-light. It's also the piece a future refactor is most likely to silently break.

func NewWebSearchStep(styles *tui.StyleSet, validateFn ValidateWebSearchKeyFunc) *WebSearchStep {
choose := components.NewSingleSelect(
[]components.SingleSelectItem{
{Label: "Tavily (Recommended)", Value: "tavily", Description: "LLM-optimized search with structured results", Icon: "🔍"},

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (UX): the old flow auto-detected an already-set TAVILY_API_KEY/PERPLEXITY_API_KEY before any prompt; the new flow makes the user pick a provider first and only then auto-skips the key. Explicit choice is a defensible improvement — but consider annotating the option that already has a key in env (e.g. "Tavily (key detected in env)") so users don't pick Perplexity not knowing their Tavily key would have been adopted silently.

}

// initKeyInput creates a fresh SecretInput for the provider API key.
func (s *WebSearchStep) initKeyInput(suffix string) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (pre-existing, carried over from the old step): this fresh SecretInput never receives a WindowSizeMsg until the next terminal resize — the validating phase swallows them — so after a validation failure the retry input renders at zero/stale width. Not a regression, just noting it survives the rewrite.

…sts (#263 review)

Blocking fix from the #263 review: the clobber guard only protected the SKIP
path. When the user picked a web-search provider interactively, ctx.BuiltinTools
was [web_search] and REPLACED a --tools flag value — dropping the flag's other
entries from forge.yaml and the init_egress DefaultToolDomains derivation.

- Extract mergeBuiltinTools(flag, fromWizard): union, first-seen order, dedupe,
  drop empties — used in collectInteractive instead of the overwrite. Table
  test pins all three paths (flag+skip, flag+provider, no-flag+provider) plus
  dedupe/empty edges.
- web_search_step: annotate a provider whose key is already in the environment
  ("· key detected in env") so the user knows selecting it adopts that key and
  picking the other provider forgoes it (review UX nit).
- Add WebSearchStep.Apply test covering the three behaviors: skip writes
  nothing; chosen provider writes WEB_SEARCH_PROVIDER + web_search + key;
  env-key path writes the provider but not the key.
@initializ-mk

Copy link
Copy Markdown
Contributor Author

Thanks — all addressed in the latest commit.

Blocking: --tools clobber (init.go). Fixed as you suggested — extracted mergeBuiltinTools(flag, fromWizard) (union, first-seen order, dedupe, drops empties) and used it instead of the overwrite. So forge init --tools web_search,http_request + choosing Tavily now yields builtin_tools: [http_request, web_search] (both reach forge.yaml and init_egress). Table test TestMergeBuiltinTools pins all three paths (flag+skip / flag+provider / no-flag+provider) plus the dedupe and empty edges.

WebSearchStep.Apply test. Added — covers the three behaviors: skip writes nothing; a chosen provider writes WEB_SEARCH_PROVIDER + web_search + key; the env-key path writes the provider but not the key.

UX nit — env-key annotation. Implemented: a provider whose key is already set gets · key detected in env appended to its label, so the user sees that picking it adopts the key (and picking the other forgoes it).

Minors left as-is (per your "harmless"/"not a regression" notes):

  • runValidation's validateFn == nil branch — left as defensive dead code.
  • The SecretInput zero-width-after-validation-failure nit — pre-existing, carried over; not touched here.

Docs semantics drift (builtin_tools meaning). Agreed it's worth a pass and it's adjacent to #281 — I'd prefer to fold the platform-policy.md / forge.yaml.tmpl wording update into #281 rather than expand this PR's scope, since that issue is specifically about the tools:-doesn't-register confusion. Shout if you'd rather it ride here.

Tests + lint green.

@initializ-mk initializ-mk merged commit 2e77273 into main Jul 13, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant