200 lines
5.4 KiB
Go
200 lines
5.4 KiB
Go
package tui
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/charmbracelet/glamour"
|
||
"github.com/charmbracelet/lipgloss"
|
||
|
||
"nub/internal/mcpc"
|
||
"nub/internal/tool"
|
||
"nub/internal/tool/builtin"
|
||
)
|
||
|
||
// wrapForViewport bricht Text auf die gegebene Breite um. bubbles/viewport
|
||
// bricht Inhalt nicht selbst um — es zeigt ihn nur an und lässt lange
|
||
// Zeilen horizontal überlaufen. lipgloss übernimmt den (ANSI-bewussten)
|
||
// Zeilenumbruch. width<=0 (z.B. vor dem ersten WindowSizeMsg) lässt den
|
||
// Text unverändert.
|
||
func wrapForViewport(content string, width int) string {
|
||
if width <= 0 {
|
||
return content
|
||
}
|
||
return lipgloss.NewStyle().Width(width).Render(content)
|
||
}
|
||
|
||
type entryKind int
|
||
|
||
const (
|
||
entryUser entryKind = iota
|
||
entryQueued
|
||
entryAssistant
|
||
entryThinking
|
||
entryToolCall
|
||
entryToolResult
|
||
entrySystem // Hintergrund-Ereignisse, nicht vom Nutzer angestoßen (z.B. Auto-Compaction)
|
||
entryCommand // Ausgabe eines Slash-Kommandos, das der Nutzer selbst getippt hat
|
||
entryError // echte Fehler (Loop-/Provider-Fehler, fehlgeschlagene Kommandos)
|
||
entryPermission // offene "ask"-Rückfrage, siehe permission.go
|
||
entryQuestion // offene Rückfrage des question-Tools, siehe question.go
|
||
)
|
||
|
||
type entry struct {
|
||
kind entryKind
|
||
text string // bereits gestylter, fertiger Text
|
||
}
|
||
|
||
var markdownRenderer, _ = glamour.NewTermRenderer(
|
||
glamour.WithAutoStyle(),
|
||
glamour.WithWordWrap(0), // kein Wrap hier — Model.renderViewport bricht den gesamten Inhalt einmalig auf die aktuelle Breite um
|
||
)
|
||
|
||
func renderMarkdown(s string) string {
|
||
if markdownRenderer == nil || strings.TrimSpace(s) == "" {
|
||
return s
|
||
}
|
||
out, err := markdownRenderer.Render(s)
|
||
if err != nil {
|
||
return s
|
||
}
|
||
return strings.TrimRight(out, "\n")
|
||
}
|
||
|
||
func renderEntry(e entry) string {
|
||
switch e.kind {
|
||
case entryUser:
|
||
return styleUser.Render("› "+firstLine(e.text)) + tailLines(e.text)
|
||
case entryQueued:
|
||
return styleQueued.Render("⏳ "+firstLine(e.text)) + tailLines(e.text)
|
||
case entryAssistant:
|
||
return styleAssistant.Render(renderMarkdown(e.text))
|
||
case entryThinking:
|
||
return styleThinking.Render(e.text)
|
||
case entryToolCall:
|
||
return styleToolCall.Render(e.text)
|
||
case entryToolResult:
|
||
return e.text // bereits gestylt (ok/error-Farbe je nach Ergebnis)
|
||
case entrySystem:
|
||
return styleSystem.Render("· " + e.text)
|
||
case entryCommand:
|
||
return styleCommand.Render(e.text)
|
||
case entryError:
|
||
return styleError.Render("✗ " + e.text)
|
||
case entryPermission:
|
||
return e.text // bereits als umrandeter Block gerendert (renderPermissionRequest)
|
||
case entryQuestion:
|
||
return e.text // bereits als umrandeter Block gerendert (renderAskRequest)
|
||
default:
|
||
return e.text
|
||
}
|
||
}
|
||
|
||
func firstLine(s string) string {
|
||
if i := strings.IndexByte(s, '\n'); i >= 0 {
|
||
return s[:i]
|
||
}
|
||
return s
|
||
}
|
||
|
||
func tailLines(s string) string {
|
||
if i := strings.IndexByte(s, '\n'); i >= 0 {
|
||
return "\n" + s[i+1:]
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// renderToolCall zeigt Name + kompakte Eingabe.
|
||
func renderToolCall(name string, input []byte) string {
|
||
in := strings.TrimSpace(prettyJSON(input))
|
||
if len(in) > 300 {
|
||
in = in[:300] + "…"
|
||
}
|
||
return fmt.Sprintf("→ %s %s", name, in)
|
||
}
|
||
|
||
// renderToolResult wählt je nach ForUI-Typ eine spezifische Darstellung
|
||
// (Diff-Overlay für write/edit, kompakte Zusammenfassung sonst).
|
||
func renderToolResult(name string, res tool.Result) string {
|
||
status := styleToolOK.Render("✓")
|
||
if res.IsError {
|
||
status = styleToolErr.Render("✗")
|
||
}
|
||
header := fmt.Sprintf("%s %s", status, name)
|
||
|
||
switch ui := res.ForUI.(type) {
|
||
case builtin.DiffResult:
|
||
return header + "\n" + renderDiff(ui)
|
||
case builtin.BashResult:
|
||
out := res.ForModel
|
||
if len(out) > 2000 {
|
||
out = out[:2000] + "\n… [truncated in TUI, see ForModel für vollen Output]"
|
||
}
|
||
return header + fmt.Sprintf(" (exit %d)\n%s", ui.ExitCode, out)
|
||
case builtin.FileResult:
|
||
return header + fmt.Sprintf(" %s (%d Zeilen)", ui.Path, ui.Lines)
|
||
case builtin.GlobResult:
|
||
return header + fmt.Sprintf(" %d Treffer", len(ui.Paths))
|
||
case builtin.GrepResult:
|
||
return header + fmt.Sprintf(" %d Treffer", ui.Count)
|
||
case builtin.TodoResult:
|
||
return header + "\n" + renderTodos(ui)
|
||
case builtin.SkillResult:
|
||
return header + fmt.Sprintf(" %s geladen (%s)", ui.Name, ui.Dir)
|
||
case mcpc.Result:
|
||
return header + " " + res.ForModel
|
||
case builtin.QuestionResult:
|
||
return fmt.Sprintf("%s\nFrage: %s\nAntwort: %s", header, ui.Question, ui.Answer)
|
||
default:
|
||
out := res.ForModel
|
||
if len(out) > 1000 {
|
||
out = out[:1000] + "…"
|
||
}
|
||
return header + "\n" + out
|
||
}
|
||
}
|
||
|
||
func renderDiff(d builtin.DiffResult) string {
|
||
var b strings.Builder
|
||
fmt.Fprintf(&b, "%s\n", d.Path)
|
||
for _, line := range d.Lines {
|
||
switch line.Op {
|
||
case builtin.DiffAdd:
|
||
b.WriteString(styleDiffAdd.Render("+ " + line.Text))
|
||
case builtin.DiffRemove:
|
||
b.WriteString(styleDiffDel.Render("- " + line.Text))
|
||
default:
|
||
b.WriteString(" " + line.Text)
|
||
}
|
||
b.WriteString("\n")
|
||
}
|
||
return strings.TrimRight(b.String(), "\n")
|
||
}
|
||
|
||
func renderTodos(t builtin.TodoResult) string {
|
||
var b strings.Builder
|
||
for _, it := range t.Items {
|
||
mark := " "
|
||
switch it.Status {
|
||
case "in_progress":
|
||
mark = "~"
|
||
case "completed":
|
||
mark = "x"
|
||
}
|
||
fmt.Fprintf(&b, "[%s] %s\n", mark, it.Content)
|
||
}
|
||
return strings.TrimRight(b.String(), "\n")
|
||
}
|
||
|
||
func prettyJSON(raw json.RawMessage) string {
|
||
var v any
|
||
if err := json.Unmarshal(raw, &v); err != nil {
|
||
return string(raw)
|
||
}
|
||
out, err := json.MarshalIndent(v, "", " ")
|
||
if err != nil {
|
||
return string(raw)
|
||
}
|
||
return string(out)
|
||
}
|