Files
strix/strix/interface/tui/internal/render/registry.go
T
Ahmed Allam e833278499 ui: render the coverage ledger, threat model, and calibration fields
The six state tools added with the coverage ledger and the threat model had
no renderer in either UI, so they fell through to the generic fallback: a raw
key/value dump of the arguments, which printed a whole threat-model document
inline as one value. The five calibration fields on a vulnerability report
were likewise reaching the markdown report and SARIF but not the screen, so
the agent's own confidence and the case against a finding were invisible to
anyone watching the scan.

Go TUI gets a coverage renderer (outcome-colored rows, state transitions, and
the ledger's history and author) and a threat-model renderer (staleness,
amendments, and a heading-level preview instead of the full document), both
registered in the dispatch switch. list_coverage and get_threat_model join the
output-heavy tools that collapse to a preview.

The React viewer gets the same two as tool families, so an unknown future
tool matching /coverage/ or /threat_model/ lands on the right renderer rather
than the fallback.

Both vulnerability renderers now show confidence, its rationale,
counterevidence, the conditions that would move severity, and how a fix was
verified.
2026-08-09 12:21:32 +00:00

141 lines
4.7 KiB
Go

package render
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
)
// statusIcon ports BaseToolRenderer.status_icon.
func statusIcon(status string) (string, lipgloss.Style) {
switch status {
case "running":
return "● In progress...", Col(AmberY)
case "completed":
return "✓ Done", Col(Green)
case "failed":
return "✗ Failed", Col(SevCrit)
case "error":
return "✗ Error", Col(SevCrit)
}
return "○ Unknown", Dim()
}
// renderGenericTool ports registry._render_default_tool_widget.
func renderGenericTool(name string, args map[string]any, result any, status string) string {
var b strings.Builder
b.WriteString(Dim().Render("→ Using tool ") + Bold(Blue).Render(name) + "\n")
for _, k := range SortedKeys(args) {
b.WriteString(" " + Dim().Render(k) + ": " + StringValue(args[k]) + "\n")
}
if (status == "completed" || status == "failed" || status == "error") && result != nil {
b.WriteString(lipgloss.NewStyle().Bold(true).Render("Result: ") + StringValue(result))
} else {
icon, style := statusIcon(status)
b.WriteString(style.Render(icon))
}
return b.String()
}
// ---------------------------------------------------------------------------
// Dispatch
// ---------------------------------------------------------------------------
func Tool(data map[string]any) string {
name := StringValue(data["tool_name"])
status := StringValue(data["status"])
args, _ := data["args"].(map[string]any)
if args == nil {
args = map[string]any{}
}
result := data["result"]
switch name {
case "exec_command":
return renderExecCommand(args, result, status)
case "write_stdin":
return renderWriteStdin(args, result, status)
case "apply_patch":
return renderApplyPatch(args, result, status)
case "view_image":
return renderViewImage(args, result)
case "create_vulnerability_report":
return renderVulnerabilityReport(args, result)
case "create_dependency_report":
return renderDependencyReport(args, result)
case "list_reports":
return renderListReports(result)
case "get_report":
return renderGetReport(result)
case "respond_to_user":
return renderRespondToUser(args)
case "finish_scan":
return renderFinishScan(args)
case "think":
return renderThink(args)
case "web_search":
return renderWebSearch(args)
case "load_skill":
return renderLoadSkill(args, result)
case "create_note", "delete_note", "update_note", "list_notes", "get_note":
return renderNote(name, args, result)
case "create_todo", "list_todos", "update_todo", "mark_todo_done", "mark_todo_pending", "delete_todo":
return renderTodo(name, result)
case "record_coverage", "update_coverage", "list_coverage":
return renderCoverage(name, args, result)
case "get_threat_model", "save_threat_model", "amend_threat_model":
return renderThreatModel(name, args, result)
case "view_agent_graph", "create_agent", "send_message_to_agent", "agent_finish", "wait_for_agents", "stop_agent":
return renderAgentGraphTool(name, args, result)
case "list_requests", "view_request", "repeat_request", "list_sitemap", "view_sitemap_entry", "scope_rules":
return renderProxyTool(name, args, result, status)
}
return renderGenericTool(name, args, result, status)
}
// ---------------------------------------------------------------------------
// Collapsing: output-heavy tools (terminal, proxy) render as short block
// previews; clicking a tool in the trace expands it to the full render.
// ---------------------------------------------------------------------------
const outputPreviewLines = 10
// ToolPreviewLines returns how many lines of a tool's render are shown before
// it is collapsed; 0 means the tool is never collapsed. Only tools whose
// output can grow unbounded (terminal, patches, proxy) collapse.
func ToolPreviewLines(name string) int {
switch name {
case "exec_command", "write_stdin", "apply_patch",
"view_request", "repeat_request", "view_sitemap_entry",
"list_coverage", "get_threat_model":
return outputPreviewLines
}
return 0
}
// CollapseTool clips a full tool render to its preview size, appending a
// click-to-expand/collapse hint. It reports whether the tool has more content
// than the preview (i.e. whether it is expandable).
func CollapseTool(full, name string, expanded bool) (string, bool) {
maxLines := ToolPreviewLines(name)
if maxLines <= 0 {
return full, false
}
lines := strings.Split(full, "\n")
if len(lines) <= maxLines {
return full, false
}
if expanded {
return full + "\n" + Dim().Italic(true).Render(" ▲ click to collapse"), true
}
preview := strings.Join(lines[:maxLines], "\n")
hidden := len(lines) - maxLines
plural := "s"
if hidden == 1 {
plural = ""
}
hint := Dim().Italic(true).Render(fmt.Sprintf(" … +%d line%s — click to expand", hidden, plural))
return preview + "\n" + hint, true
}