feat(tui): surface SLM backend + per-turn classifier in status bar
The TUI gave no indication that an SLM was configured or active.
You'd see the primary provider on the status line and nothing else,
even with [slm].enabled=true and a successfully booted backend.
Two surfaces added:
1. Status-bar SLM badge. The left side of the status line gains a
dim " · slm: <model> ⚙" suffix when the backend booted, " · slm: ✗"
when it failed, and nothing when SLM is disabled. The ⚙ marker
indicates the model advertises tool support.
2. Per-turn classifier visibility. The existing routing event already
produced "routed → <arm> (task: <type>)" lines in the chat history;
it now also reports which classifier made the decision, e.g.
"routed → ollama/ministral-3:3b (task: explain, by: slm_fallback)".
Lets you tell in real time whether the SLM is actually classifying
or falling back to the keyword heuristic.
Plumbing:
- new tui.SLMInfo struct on tui.Config
- main.go populates it after StartBackend returns
- stream.Event gains RoutingClassifier; engine.runLoop fills it from
task.ClassifierSource on the first round
This commit is contained in:
@@ -0,0 +1 @@
|
||||
{"sessionId":"837af2d6-1ee9-4eab-8a71-097f47600e06","pid":65427,"procStart":"219330","acquiredAt":1779201696258}
|
||||
@@ -671,7 +671,9 @@ func main() {
|
||||
lazy := &lazyClassifier{logger: logger}
|
||||
var engineClassifier router.TaskClassifier = lazy
|
||||
var slmCleanup func() error
|
||||
var slmInfo tui.SLMInfo
|
||||
if cfg.SLM.Enabled {
|
||||
slmInfo.Enabled = true
|
||||
bcfg := slm.BackendConfig{
|
||||
Backend: slm.Backend(cfg.SLM.Backend),
|
||||
Model: cfg.SLM.Model,
|
||||
@@ -704,6 +706,10 @@ func main() {
|
||||
Capabilities: provider.Capabilities{ToolUse: boot.ToolSupport},
|
||||
})
|
||||
slmCleanup = boot.Close
|
||||
slmInfo.Active = true
|
||||
slmInfo.Backend = string(boot.Backend)
|
||||
slmInfo.Model = boot.Model
|
||||
slmInfo.Tools = boot.ToolSupport
|
||||
toolNote := "no tools"
|
||||
if boot.ToolSupport {
|
||||
toolNote = "tools"
|
||||
@@ -914,6 +920,7 @@ func main() {
|
||||
PluginInfos: buildPluginInfos(discoveredPlugins, enabledSet),
|
||||
Version: buildVersion,
|
||||
ModelUpdateCh: modelUpdateCh,
|
||||
SLM: slmInfo,
|
||||
})
|
||||
p := tea.NewProgram(m)
|
||||
if _, err := p.Run(); err != nil {
|
||||
|
||||
@@ -134,9 +134,10 @@ func (e *Engine) runLoop(ctx context.Context, cb Callback) (*Turn, error) {
|
||||
)
|
||||
if turn.Rounds == 1 && cb != nil {
|
||||
cb(stream.Event{
|
||||
Type: stream.EventRouting,
|
||||
RoutingModel: string(decision.Arm.ID),
|
||||
RoutingTask: task.Type.String(),
|
||||
Type: stream.EventRouting,
|
||||
RoutingModel: string(decision.Arm.ID),
|
||||
RoutingTask: task.Type.String(),
|
||||
RoutingClassifier: task.ClassifierSource.String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +76,9 @@ type Event struct {
|
||||
Usage *message.Usage
|
||||
|
||||
// Routing — arm selected by router
|
||||
RoutingModel string // e.g. "anthropic/claude-sonnet-4-20250514"
|
||||
RoutingTask string // classified task type
|
||||
RoutingModel string // e.g. "anthropic/claude-sonnet-4-20250514"
|
||||
RoutingTask string // classified task type
|
||||
RoutingClassifier string // classifier source: heuristic / slm / slm_fallback
|
||||
|
||||
// Error
|
||||
Err error
|
||||
|
||||
@@ -71,6 +71,17 @@ type Config struct {
|
||||
PluginInfos []PluginInfo // discovered plugins for /plugins command
|
||||
Version string // build version string (from ldflags)
|
||||
ModelUpdateCh <-chan struct{} // signals when the model name changes (discovery reconciliation)
|
||||
SLM SLMInfo // SLM backend status for the status bar
|
||||
}
|
||||
|
||||
// SLMInfo captures the resolved SLM backend state at startup so the TUI can
|
||||
// surface it in the status bar. Zero value (Enabled=false) renders nothing.
|
||||
type SLMInfo struct {
|
||||
Enabled bool
|
||||
Active bool // true when StartBackend returned a usable Boot
|
||||
Backend string // resolved backend name: "ollama", "llamafile", etc.
|
||||
Model string // model identifier
|
||||
Tools bool // whether the model advertises tool support
|
||||
}
|
||||
|
||||
// PluginInfo is a summary of an installed plugin for TUI display.
|
||||
|
||||
@@ -48,10 +48,12 @@ func (m Model) handleStreamEvent(evt stream.Event) (tea.Model, tea.Cmd) {
|
||||
m.runningTools = append(m.runningTools, evt.ToolCallName)
|
||||
}
|
||||
case stream.EventRouting:
|
||||
m.messages = append(m.messages, chatMessage{
|
||||
role: "cost",
|
||||
content: fmt.Sprintf("routed → %s (task: %s)", evt.RoutingModel, evt.RoutingTask),
|
||||
})
|
||||
content := fmt.Sprintf("routed → %s (task: %s", evt.RoutingModel, evt.RoutingTask)
|
||||
if evt.RoutingClassifier != "" {
|
||||
content += ", by: " + evt.RoutingClassifier
|
||||
}
|
||||
content += ")"
|
||||
m.messages = append(m.messages, chatMessage{role: "cost", content: content})
|
||||
case stream.EventToolResult:
|
||||
if m.elfToolActive {
|
||||
// Suppress raw elf output — tree shows progress, LLM summarizes
|
||||
|
||||
@@ -534,7 +534,7 @@ func (m Model) renderStatus() string {
|
||||
if !status.ToolsAvailable {
|
||||
provModel += " " + sStatusDim.Render("text-only")
|
||||
}
|
||||
left := sStatusHighlight.Render(provModel)
|
||||
left := sStatusHighlight.Render(provModel) + renderSLMBadge(m.config.SLM)
|
||||
|
||||
// Center: cwd + git branch
|
||||
dir := filepath.Base(m.cwd)
|
||||
@@ -615,6 +615,22 @@ func renderContextBar(s session.Status) string {
|
||||
return "[" + bar + "]" + labelStyle.Render(label)
|
||||
}
|
||||
|
||||
// renderSLMBadge produces a short " · slm: <model> [tools]" badge for the
|
||||
// status bar's left side. Returns "" when SLM is disabled or unconfigured.
|
||||
func renderSLMBadge(info SLMInfo) string {
|
||||
if !info.Enabled {
|
||||
return ""
|
||||
}
|
||||
if !info.Active {
|
||||
return sStatusDim.Render(" · slm: ✗")
|
||||
}
|
||||
label := " · slm: " + info.Model
|
||||
if info.Tools {
|
||||
label += " ⚙"
|
||||
}
|
||||
return sStatusDim.Render(label)
|
||||
}
|
||||
|
||||
// formatTurnUsage produces a compact token summary for a single turn.
|
||||
func formatTurnUsage(u message.Usage) string {
|
||||
parts := []string{fmt.Sprintf("in: %d", u.InputTokens), fmt.Sprintf("out: %d", u.OutputTokens)}
|
||||
|
||||
Reference in New Issue
Block a user