Files
strix/strix/interface/tui/internal/render/proxy.go
T

578 lines
16 KiB
Go

package render
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
)
// ---------------------------------------------------------------------------
// Proxy (proxy_renderer.py)
// ---------------------------------------------------------------------------
const proxyIcon = "<~>"
func proxyStatusStyle(code int) lipgloss.Style {
switch {
case code >= 200 && code < 300:
return Col(Green)
case code >= 300 && code < 400:
return Col(Status3xx)
case code >= 400 && code < 500:
return Col(Status4xx)
case code >= 500:
return Col(Red)
}
return Dim()
}
func ptrunc(s string, max int) string {
if len(s) > max {
return s[:max-3] + "..."
}
return s
}
func psanitize(s string, max int) string {
clean := strings.NewReplacer("\n", " ", "\r", "", "\t", " ").Replace(s)
return ptrunc(clean, max)
}
func renderProxyTool(name string, args map[string]any, result any, status string) string {
switch name {
case "list_requests":
return renderListRequests(args, result, status)
case "view_request":
return renderViewRequest(args, result, status)
case "repeat_request":
return renderRepeatRequest(args, result, status)
case "list_sitemap":
return renderListSitemap(args, result, status)
case "view_sitemap_entry":
return renderViewSitemapEntry(args, result, status)
case "scope_rules":
return renderScopeRules(args, result, status)
}
return ""
}
func resultMapOf(result any) (map[string]any, bool) {
m, ok := result.(map[string]any)
return m, ok
}
func renderListRequests(args map[string]any, result any, status string) string {
var b strings.Builder
b.WriteString(Dim().Render(proxyIcon) + Col(Cyan).Render(" listing requests"))
if f := StringValue(args["httpql_filter"]); f != "" {
b.WriteString(Dim().Italic(true).Render(" where " + ptrunc(f, 150)))
}
var meta []string
if s := StringValue(args["sort_by"]); s != "" && s != "timestamp" {
meta = append(meta, "by:"+s)
}
if s := StringValue(args["sort_order"]); s != "" && s != "desc" {
meta = append(meta, s)
}
if s := StringValue(args["scope_id"]); s != "" {
meta = append(meta, "scope:"+truncStr(s, 8))
}
if len(meta) > 0 {
b.WriteString(Dim().Render(" (" + strings.Join(meta, ", ") + ")"))
}
if status == "completed" {
if m, ok := resultMapOf(result); ok {
if e, has := m["error"]; has {
b.WriteString(Col(Red).Render(" error: " + psanitize(StringValue(e), 150)))
} else {
entries, _ := m["entries"].([]any)
suffix := ""
if pi, ok := m["page_info"].(map[string]any); ok && truthy(pi["has_next_page"]) {
suffix = "+"
}
b.WriteString(Dim().Render(fmt.Sprintf(" [%d%s found]", len(entries), suffix)))
renderRequestEntries(&b, entries)
}
}
}
return b.String()
}
func renderRequestEntries(b *strings.Builder, entries []any) {
if len(entries) == 0 {
return
}
b.WriteString("\n")
limit := len(entries)
if limit > 20 {
limit = 20
}
for i := 0; i < limit; i++ {
entry, ok := entries[i].(map[string]any)
if !ok {
continue
}
req, _ := entry["request"].(map[string]any)
resp, _ := entry["response"].(map[string]any)
method := StringValue(req["method"])
if method == "" {
method = "?"
}
host := StringValue(req["host"])
path := StringValue(req["path"])
if path == "" {
path = "/"
}
b.WriteString(" " + Col(Lavender).Render(fmt.Sprintf("%-6s", method)))
b.WriteString(Dim().Render(" " + ptrunc(host+path, 180)))
if code, ok := NumericValue(resp["status_code"]); ok && code != 0 {
b.WriteString(proxyStatusStyle(int(code)).Render(fmt.Sprintf(" %d", int(code))))
}
if i < limit-1 {
b.WriteString("\n")
}
}
if len(entries) > 20 {
b.WriteString("\n" + Dim().Italic(true).Render(fmt.Sprintf(" ... +%d more", len(entries)-20)))
}
}
func renderViewRequest(args map[string]any, result any, status string) string {
var b strings.Builder
b.WriteString(Dim().Render(proxyIcon))
part := StringValue(args["part"])
if part == "" {
part = "request"
}
action := "viewing"
search := StringValue(args["search_pattern"])
if search != "" {
action = "searching"
}
b.WriteString(Col(Cyan).Render(" " + action + " " + part))
if rid := StringValue(args["request_id"]); rid != "" {
b.WriteString(Dim().Render(" #" + rid))
}
if search != "" {
b.WriteString(Dim().Italic(true).Render(" /" + ptrunc(search, 100) + "/"))
}
if status == "completed" {
if m, ok := resultMapOf(result); ok {
if e, has := m["error"]; has {
b.WriteString(Col(Red).Render(" error: " + psanitize(StringValue(e), 150)))
} else if hits, has := m["hits"].([]any); has {
total := len(hits)
if t, ok := NumericValue(m["total_hits"]); ok {
total = int(t)
}
b.WriteString(Dim().Render(fmt.Sprintf(" [%d matches]", total)))
renderSearchHits(&b, hits)
} else if content, has := m["content"]; has {
page := 1
if p, ok := NumericValue(m["page"]); ok {
page = int(p)
}
tl := 0
if t, ok := NumericValue(m["total_lines"]); ok {
tl = int(t)
}
b.WriteString(Dim().Render(fmt.Sprintf(" [page %d, %d lines]", page, tl)))
renderContentLines(&b, StringValue(content), truthy(m["has_more"]))
}
}
}
return b.String()
}
func renderSearchHits(b *strings.Builder, hits []any) {
if len(hits) == 0 {
return
}
b.WriteString("\n")
limit := len(hits)
if limit > 5 {
limit = 5
}
for i := 0; i < limit; i++ {
m, ok := hits[i].(map[string]any)
if !ok {
continue
}
before := lastN(strings.NewReplacer("\n", " ", "\r", "").Replace(StringValue(m["before"])), 100)
after := firstN(strings.NewReplacer("\n", " ", "\r", "").Replace(StringValue(m["after"])), 100)
b.WriteString(" ")
if before != "" {
b.WriteString(Dim().Render("..." + before))
}
b.WriteString(Bold(Green).Render(StringValue(m["match"])))
if after != "" {
b.WriteString(Dim().Render(after + "..."))
}
if i < limit-1 {
b.WriteString("\n")
}
}
if len(hits) > 5 {
b.WriteString("\n" + Dim().Italic(true).Render(fmt.Sprintf(" ... +%d more matches", len(hits)-5)))
}
}
func renderContentLines(b *strings.Builder, content string, hasMore bool) {
if content == "" {
return
}
allLines := strings.Split(content, "\n")
lines := allLines
if len(lines) > 15 {
lines = lines[:15]
}
b.WriteString("\n")
for i, line := range lines {
b.WriteString(" " + Dim().Render(ptrunc(line, maxLineLength)))
if i < len(lines)-1 {
b.WriteString("\n")
}
}
if hasMore || len(allLines) > 15 {
b.WriteString("\n" + Dim().Italic(true).Render(" ... more content available"))
}
}
func renderRepeatRequest(args map[string]any, result any, status string) string {
var b strings.Builder
b.WriteString(Dim().Render(proxyIcon) + Col(Cyan).Render(" repeating request"))
if rid := StringValue(args["request_id"]); rid != "" {
b.WriteString(Dim().Render(" #" + rid))
}
if mods, ok := args["modifications"].(map[string]any); ok {
b.WriteString(Dim().Italic(true).Render("\n modifications:"))
arrow := Col(Blue).Render(" >> ")
if url, ok := mods["url"]; ok {
b.WriteString("\n" + arrow + Dim().Render("url: "+ptrunc(StringValue(url), 180)))
}
writeKV := func(key, prefix string, valMax int) {
if kv, ok := mods[key].(map[string]any); ok {
n := 0
for k, v := range kv {
if n >= 5 {
break
}
b.WriteString("\n" + arrow + Dim().Render(fmt.Sprintf(prefix, k, psanitize(StringValue(v), valMax))))
n++
}
}
}
writeKV("headers", "%s: %s", 150)
writeKV("cookies", "cookie %s=%s", 100)
writeKV("params", "param %s=%s", 100)
if body, ok := mods["body"].(string); ok {
b.WriteString("\n" + arrow)
bodyLines := strings.Split(body, "\n")
shown := bodyLines
if len(shown) > 4 {
shown = shown[:4]
}
for i, line := range shown {
if i > 0 {
b.WriteString("\n" + Dim().Render(" "))
}
b.WriteString(Dim().Render(ptrunc(line, maxLineLength)))
}
if len(bodyLines) > 4 {
b.WriteString(Dim().Italic(true).Render(" ..."))
}
}
} else if mods, ok := args["modifications"].(string); ok && mods != "" {
b.WriteString(Dim().Italic(true).Render("\n " + ptrunc(mods, 200)))
}
if status == "completed" {
if m, ok := resultMapOf(result); ok {
success, hasSuccess := m["success"].(bool)
if hasSuccess && !success && StringValue(m["error"]) != "" {
b.WriteString(Col(Red).Render("\n error: " + psanitize(StringValue(m["error"]), 150)))
} else {
resp, _ := m["response"].(map[string]any)
b.WriteString("\n" + Col(Green).Render(" << "))
if code, ok := NumericValue(resp["status_code"]); ok && code != 0 {
b.WriteString(proxyStatusStyle(int(code)).Render(fmt.Sprintf("%d", int(code))))
} else {
b.WriteString(Dim().Render("(no response)"))
}
if ms, ok := NumericValue(m["elapsed_ms"]); ok && ms != 0 {
b.WriteString(Dim().Render(fmt.Sprintf(" (%dms)", int(ms))))
}
body := StringValue(resp["body"])
if body != "" {
allLines := strings.Split(body, "\n")
lines := allLines
if len(lines) > 5 {
lines = lines[:5]
}
for _, line := range lines {
b.WriteString("\n" + Col(Green).Render(" << ") + Dim().Render(ptrunc(line, maxLineLength-5)))
}
if truthy(resp["body_truncated"]) || len(allLines) > 5 {
b.WriteString("\n" + Dim().Italic(true).Render(" ..."))
}
}
}
}
}
return b.String()
}
func renderListSitemap(args map[string]any, result any, status string) string {
var b strings.Builder
b.WriteString(Dim().Render(proxyIcon) + Col(Cyan).Render(" listing sitemap"))
if pid := StringValue(args["parent_id"]); pid != "" {
b.WriteString(Dim().Render(" under #" + ptrunc(pid, 20)))
}
var meta []string
if s := StringValue(args["scope_id"]); s != "" {
meta = append(meta, "scope:"+truncStr(s, 8))
}
if d := StringValue(args["depth"]); d != "" && d != "DIRECT" {
meta = append(meta, strings.ToLower(d))
}
if len(meta) > 0 {
b.WriteString(Dim().Render(" (" + strings.Join(meta, ", ") + ")"))
}
if status == "completed" {
if m, ok := resultMapOf(result); ok {
if e, has := m["error"]; has {
b.WriteString(Col(Red).Render(" error: " + psanitize(StringValue(e), 150)))
} else {
total := 0
if t, ok := NumericValue(m["total_count"]); ok {
total = int(t)
}
entries, _ := m["entries"].([]any)
b.WriteString(Dim().Render(fmt.Sprintf(" [%d entries]", total)))
renderSitemapEntries(&b, entries)
}
}
}
return b.String()
}
var sitemapKindColors = map[string]lipgloss.Color{
"DOMAIN": AmberY, "DIRECTORY": Blue, "REQUEST": Green,
}
func renderSitemapEntries(b *strings.Builder, entries []any) {
if len(entries) == 0 {
return
}
b.WriteString("\n")
limit := len(entries)
if limit > 20 {
limit = 20
}
for i := 0; i < limit; i++ {
entry, ok := entries[i].(map[string]any)
if !ok {
continue
}
kind := StringValue(entry["kind"])
if kind == "" {
kind = "?"
}
label := StringValue(entry["label"])
if label == "" {
label = "?"
}
kindStyle, ok := sitemapKindColors[kind]
style := Dim()
if ok {
style = Col(kindStyle)
}
abbr := kind
if len(abbr) > 3 {
abbr = abbr[:3]
}
b.WriteString(" " + style.Render(fmt.Sprintf("%-3s", abbr)) + Dim().Render(" "+ptrunc(label, 150)))
if req, ok := entry["request"].(map[string]any); ok {
if method := StringValue(req["method"]); method != "" {
b.WriteString(Col(Lavender).Render(" " + method))
}
if code, ok := NumericValue(req["status_code"]); ok && code != 0 {
b.WriteString(proxyStatusStyle(int(code)).Render(fmt.Sprintf(" %d", int(code))))
}
}
if truthy(entry["has_descendants"]) {
b.WriteString(Dim().Italic(true).Render(" +"))
}
if i < limit-1 {
b.WriteString("\n")
}
}
if len(entries) > 20 {
b.WriteString("\n" + Dim().Italic(true).Render(fmt.Sprintf(" ... +%d more", len(entries)-20)))
}
}
func renderViewSitemapEntry(args map[string]any, result any, status string) string {
var b strings.Builder
b.WriteString(Dim().Render(proxyIcon) + Col(Cyan).Render(" viewing sitemap"))
if eid := StringValue(args["entry_id"]); eid != "" {
b.WriteString(Dim().Render(" #" + ptrunc(eid, 20)))
}
if status == "completed" {
if m, ok := resultMapOf(result); ok {
if e, has := m["error"]; has {
b.WriteString(Col(Red).Render(" error: " + psanitize(StringValue(e), 150)))
} else if entry, ok := m["entry"].(map[string]any); ok {
kind, label := StringValue(entry["kind"]), StringValue(entry["label"])
related, _ := entry["related_requests"].(map[string]any)
if kind != "" && label != "" {
b.WriteString(Dim().Render(fmt.Sprintf(" %s: %s", kind, ptrunc(label, 120))))
}
total := 0
if t, ok := NumericValue(related["total_count"]); ok {
total = int(t)
}
if total != 0 {
b.WriteString(Dim().Render(fmt.Sprintf(" [%d requests]", total)))
}
reqs, _ := related["requests"].([]any)
renderRelatedRequests(&b, reqs)
}
}
}
return b.String()
}
func renderRelatedRequests(b *strings.Builder, reqs []any) {
if len(reqs) == 0 {
return
}
b.WriteString("\n")
limit := len(reqs)
if limit > 10 {
limit = 10
}
for i := 0; i < limit; i++ {
req, ok := reqs[i].(map[string]any)
if !ok {
continue
}
method := StringValue(req["method"])
if method == "" {
method = "?"
}
path := StringValue(req["path"])
if path == "" {
path = "/"
}
b.WriteString(" " + Col(Lavender).Render(fmt.Sprintf("%-6s", method)) + Dim().Render(" "+ptrunc(path, 180)))
if code, ok := NumericValue(req["status_code"]); ok && code != 0 {
b.WriteString(proxyStatusStyle(int(code)).Render(fmt.Sprintf(" %d", int(code))))
}
if i < limit-1 {
b.WriteString("\n")
}
}
if len(reqs) > 10 {
b.WriteString("\n" + Dim().Italic(true).Render(fmt.Sprintf(" ... +%d more", len(reqs)-10)))
}
}
var scopeActionMap = map[string]string{
"get": "getting", "list": "listing", "create": "creating", "update": "updating", "delete": "deleting",
}
func renderScopeRules(args map[string]any, result any, status string) string {
var b strings.Builder
b.WriteString(Dim().Render(proxyIcon))
action := StringValue(args["action"])
actionText, ok := scopeActionMap[action]
if !ok {
if action != "" {
actionText = action + "ing"
} else {
actionText = "managing"
}
}
b.WriteString(Col(Cyan).Render(" " + actionText + " proxy scope"))
if sn := StringValue(args["scope_name"]); sn != "" {
b.WriteString(Dim().Italic(true).Render(" '" + ptrunc(sn, 50) + "'"))
}
if sid := StringValue(args["scope_id"]); sid != "" {
b.WriteString(Dim().Render(" #" + truncStr(sid, 8)))
}
writeList := func(key, label string) {
if items, ok := args[key].([]any); ok && len(items) > 0 {
shown := items
if len(shown) > 4 {
shown = shown[:4]
}
var parts []string
for _, it := range shown {
parts = append(parts, ptrunc(StringValue(it), 40))
}
b.WriteString("\n " + Dim().Render(label+": "+strings.Join(parts, ", ")))
if len(items) > 4 {
b.WriteString(Dim().Italic(true).Render(fmt.Sprintf(" +%d", len(items)-4)))
}
}
}
writeList("allowlist", "allow")
writeList("denylist", "deny")
if status == "completed" {
if m, ok := resultMapOf(result); ok {
switch {
case m["error"] != nil:
b.WriteString(Col(Red).Render(" error: " + psanitize(StringValue(m["error"]), 150)))
case m["scopes"] != nil:
scopes, _ := m["scopes"].([]any)
b.WriteString(Dim().Render(fmt.Sprintf(" [%d scopes]", len(scopes))))
renderScopeList(&b, scopes)
case m["scope"] != nil:
if scope, ok := m["scope"].(map[string]any); ok {
if allow, ok := scope["allowlist"].([]any); ok && len(allow) > 0 {
b.WriteString("\n " + Dim().Render("allow: "+joinTrunc(allow, 40, 5)))
}
if deny, ok := scope["denylist"].([]any); ok && len(deny) > 0 {
b.WriteString("\n " + Dim().Render("deny: "+joinTrunc(deny, 40, 5)))
}
}
case m["message"] != nil:
b.WriteString(Col(Green).Render(" " + StringValue(m["message"])))
}
}
}
return b.String()
}
func renderScopeList(b *strings.Builder, scopes []any) {
if len(scopes) == 0 {
return
}
b.WriteString("\n")
limit := len(scopes)
if limit > 5 {
limit = 5
}
for i := 0; i < limit; i++ {
scope, ok := scopes[i].(map[string]any)
if !ok {
continue
}
name := StringValue(scope["name"])
if name == "" {
name = "?"
}
b.WriteString(" " + Col(Green).Render(ptrunc(name, 40)))
if allow, ok := scope["allowlist"].([]any); ok && len(allow) > 0 {
b.WriteString(Dim().Render(" " + joinTrunc(allow, 30, 3)))
if len(allow) > 3 {
b.WriteString(Dim().Italic(true).Render(fmt.Sprintf(" +%d", len(allow)-3)))
}
}
if i < limit-1 {
b.WriteString("\n")
}
}
}