Files
zero/internal/tui/options.go
KRATOS 284a6c86cb Skills: deterministic invocation — /<skill-name>, searchable /skills picker, discovery-budget fix (#520)
* feat(skills): make skill invocation deterministic and discoverable

Skills were model-pulled only: the model had to notice a skill in a
640-byte system-prompt list (~6 skills visible; the rest collapsed into
'…and N more') and call the skill tool on its own. Users could not run a
skill at all — typing /<skill-name> reported 'unknown command'. The two
halves of the fix:

User invocation
- /<skill-name> [args] now runs an installed skill directly: resolved
  after builtins and user commands, the skill body is inlined as the
  prompt with the typed args appended, and submitted through the same
  launch path user commands use.
- /skills lists installed skills with their slash form, live from disk.
  Labels are honest: a skill shadowed by a builtin, alias, user command,
  or a case-colliding sibling is listed bare with a '(via skill tool)'
  note instead of advertising a dead /name.
- The slash palette suggests skills (labeled '(skill)') read through the
  same loader dispatch uses, so mid-session installs appear and shadowed
  or case-colliding names are never advertised.
- Skill and user-command invocations now apply the plain-prompt run-state
  guards: mid-run submissions queue the EXPANDED prompt (the queue flush
  submits literal text), compaction-in-flight warns, exiting drops.
- Skills resolve through a lazy plugin-aware loader (default dir merged
  with plugin skill roots — the same set the skill tool and the system
  prompt see), TTL-cached so palette keystrokes don't re-read SKILL.md
  bodies.

Model triggering
- Raise the skills-list budget 640 -> 4096 bytes: the list is the model's
  only discovery surface, and the old cap made skills past ~6 invisible —
  invocation reliability collapsed exactly for users invested in skills.
- Raise description truncation 100 -> 200 runes: the description carries
  the trigger conditions ('use when…'); cutting them is what stopped the
  model from firing the skill.
- Strengthen the instruction line: scan the list before acting, call the
  matching skill FIRST, don't substitute your own approach.

* feat(tui): /skills opens a searchable skill picker

Replace the static /skills card with the same picker UX /model uses:
type-to-filter (ranked search), arrow-key selection, Enter runs the
chosen skill immediately. Selection is by the skill's exact name, so
skills the slash path cannot reach — names shadowed by a builtin or
user command, or names that don't fit a slash token — are still
runnable from the picker, where the choice is unambiguous. The text
card remains only as the no-skills install hint, and the picker path
applies the same run-state guards as slash invocation.

* feat(tui): ask-first guard for bare skill invocations

A skill invoked with no request submits only its body — instructions
with no target ("review the PR" — which PR?) — and the model improvises
a target instead of asking, which is especially wrong in a fresh
session. Bare invocations (slash and picker) now append a conditional
note: if the instructions need a target or details not already clear
from the conversation, ask first; self-contained skills still just run.
Invocations that carry a request are unchanged.

* fix(tui): review follow-ups — restore typed invocation on compaction guard, cap merged suggestions

- The compaction-in-flight guard for skill/user-command invocations fired
  after the dispatch path had already cleared the composer, so the typed
  "/name args" was lost and had to be retyped; thread the raw invocation
  through and restore it into the composer (the plain-prompt guard keeps
  the composer intact by firing before the clear). The picker path has no
  typed form and restores nothing.
- Cap the merged autocomplete list at the shared bound: each source
  self-caps, but three sources could stack to ~3x the palette limit.
- Tighten the shadowed-skill palette test to assert on the advertised
  Name rather than the fallback Desc wording.

* chore: ignore zero-skills local build artifact

A locally built test binary under this name was accidentally committed
(and has been scrubbed from the branch history); ignore the name so it
cannot recur.
2026-07-05 21:14:53 +05:30

135 lines
4.7 KiB
Go

package tui
import (
"context"
tea "charm.land/bubbletea/v2"
"github.com/Gitlawb/zero/internal/agent"
"github.com/Gitlawb/zero/internal/config"
"github.com/Gitlawb/zero/internal/mcp"
"github.com/Gitlawb/zero/internal/modelregistry"
"github.com/Gitlawb/zero/internal/providerhealth"
"github.com/Gitlawb/zero/internal/providermodeldiscovery"
"github.com/Gitlawb/zero/internal/sandbox"
"github.com/Gitlawb/zero/internal/sessions"
"github.com/Gitlawb/zero/internal/skills"
"github.com/Gitlawb/zero/internal/tools"
"github.com/Gitlawb/zero/internal/usage"
"github.com/Gitlawb/zero/internal/zeroruntime"
)
// Options configures the reusable Zero terminal UI shell.
type Options struct {
Cwd string
UserConfigPath string
DoctorUserConfigPath string
ProjectConfigPath string
ProviderName string
ModelName string
ProviderProfile config.ProviderProfile
SavedProviders []config.ProviderProfile // all configured providers, for the /model multi-provider list
FavoriteModels []string
RecapsEnabled bool
Provider zeroruntime.Provider
NewProvider func(config.ProviderProfile) (zeroruntime.Provider, error)
ProbeProviderHealth func(context.Context, providerhealth.Options) providerhealth.Result
DiscoverProviderModels func(context.Context, config.ProviderProfile) ([]providermodeldiscovery.Model, error)
DiscoverOllamaContextWindow func(ctx context.Context, baseURL string, model string) (int, error)
RuntimeMessageSink func(tea.Msg)
Registry *tools.Registry
SessionStore *sessions.Store
SandboxStore *sandbox.GrantStore
MCPConfig config.MCPConfig
MCPPermissionStore *mcp.PermissionStore
MCPTokenStore *mcp.TokenStore
MCPCommand func(context.Context, []string) MCPCommandResult
SandboxSetupCommand func(context.Context) SandboxSetupCommandResult
UsageTracker *usage.Tracker
SessionCompactor SessionCompactor
PrService *PrService
AgentOptions agent.Options
// LoadSkills returns the installed skills (default skills dir merged with any
// plugin skill roots), bodies included, for /skills and direct /<skill-name>
// invocation. Called lazily per use so newly installed skills are picked up
// without a restart. Nil means the session has no skills wiring (skills stay
// model-pulled via the skill tool only).
LoadSkills func() []skills.Skill
PermissionMode agent.PermissionMode
ReasoningEffort modelregistry.ReasoningEffort
ResponseStyle string
// Theme is the operator's palette preference: "auto" (default), a built-in
// ("dark"/"light"), or a registered color theme. Set from the --theme flag;
// falls back to ZERO_THEME, then the persisted SavedTheme, then auto.
Theme string
// SavedTheme is the theme persisted in user config (Preferences.Theme). Applied
// at startup below --theme and ZERO_THEME, so a /theme choice survives restart.
SavedTheme string
UserAgent string
// Notify configures completion / awaiting-input notifications.
Notify config.NotifyConfig
// KeyBindings configures remappable TUI keybindings. An empty/zero
// KeyBindingsConfig means "use built-in defaults" for each action.
KeyBindings config.KeyBindingsConfig
// AltScreen tells the model it is running inside Bubble Tea's alternate
// screen. Run sets this for the interactive app; tests can leave it false
// to exercise the native scrollback renderer.
AltScreen bool
// Setup configures the first-run/setup takeover. It is shown before the
// normal chat surface when Visible is true.
Setup SetupOptions
}
type MCPCommandResult struct {
Config config.MCPConfig
Output string
Error string
ExitCode int
}
type SandboxSetupCommandResult struct {
Output string
Error string
ExitCode int
}
// SetupOptions configures the guided first-run provider setup takeover.
type SetupOptions struct {
Visible bool
Required bool
ConfigPath string
Providers []SetupProviderOption
Save func(SetupSelection) (SetupResult, error)
}
// SetupProviderOption is one provider choice offered by the setup takeover.
type SetupProviderOption struct {
ID string
Name string
DefaultModel string
EnvVar string
RequiresAuth bool
Local bool
Recommended bool
}
// SetupSelection is the user's setup choice.
type SetupSelection struct {
CatalogID string
Name string
BaseURL string
Model string
APIKey string
}
// SetupResult describes a completed setup write.
type SetupResult struct {
ConfigPath string
Provider config.ProviderProfile
}