mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 23:40:28 -10:00
* 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.
135 lines
4.7 KiB
Go
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
|
|
}
|