Files
correx/apps/tui-go/internal/app/artifact_preview.go
T
kami 5e98f6661e feat(tui): render structured artifacts as a summary, not raw JSON
Approval-card and preview JSON artifacts (the freestyle execution_plan, the analyst's analysis) rendered as a raw JSON dump. Add a JSON-aware preview: a scannable goal + numbered stage list for execution_plan, indented key/value lines for other objects, with light highlighting. Non-JSON previews fall back to the raw line view unchanged.
2026-07-02 13:26:42 +04:00

144 lines
4.1 KiB
Go

package app
import (
"encoding/json"
"sort"
"strings"
)
// artifactDisplayLines turns a structured (JSON) artifact preview into scannable plain-text lines,
// so the approval card and transcript show a readable summary instead of a raw JSON dump. Returns
// (nil, false) for non-JSON content, which falls back to the raw line view. The lines are plain;
// colorizePreviewLine adds the highlighting at render time. Kept in sync with previewRowCount via
// previewDisplayLines so band sizing/scroll bounds match what's drawn.
func artifactDisplayLines(content string) ([]string, bool) {
trimmed := strings.TrimSpace(content)
if !strings.HasPrefix(trimmed, "{") {
return nil, false
}
var obj map[string]json.RawMessage
if json.Unmarshal([]byte(trimmed), &obj) != nil {
return nil, false
}
if lines, ok := planSummaryLines(obj); ok {
return lines, true
}
return objectSummaryLines(obj), true
}
// planSummaryLines renders an execution_plan artifact ({goal, stages:[{id,role,prompt,produces,…}]})
// as a goal line plus a numbered, aligned stage list — the freestyle plan approval an operator sees.
func planSummaryLines(obj map[string]json.RawMessage) ([]string, bool) {
raw, ok := obj["stages"]
if !ok {
return nil, false
}
var stages []struct {
ID string `json:"id"`
Role string `json:"role"`
Prompt string `json:"prompt"`
Produces string `json:"produces"`
}
if json.Unmarshal(raw, &stages) != nil || len(stages) == 0 {
return nil, false
}
var out []string
if g := jsonString(obj["goal"]); g != "" {
out = append(out, "Goal", " "+g, "")
}
out = append(out, "Plan · "+plural(len(stages), "stage"))
idW := 0
for _, s := range stages {
if len(s.ID) > idW {
idW = len(s.ID)
}
}
for i, s := range stages {
head := " " + itoa(i+1) + ". " + padRaw(s.ID, idW) + " " + padRaw(s.Role, 12)
if s.Produces != "" {
head += " → " + s.Produces
}
out = append(out, head)
if p := firstLine(s.Prompt); p != "" {
out = append(out, " "+p)
}
}
return out, true
}
// objectSummaryLines pretty-prints any other JSON object as indented key/value lines — a readable
// fallback (e.g. the analyst's `analysis` artifact) instead of a single minified blob.
func objectSummaryLines(obj map[string]json.RawMessage) []string {
keys := make([]string, 0, len(obj))
for k := range obj {
keys = append(keys, k)
}
sort.Strings(keys)
var out []string
for _, k := range keys {
out = append(out, jsonFieldLines(k, obj[k], 0)...)
}
return out
}
// jsonFieldLines renders one "key: value" field, recursing into nested objects/arrays with
// indentation. Scalars go inline; containers get a header line then their children.
func jsonFieldLines(key string, raw json.RawMessage, depth int) []string {
indent := strings.Repeat(" ", depth)
v := strings.TrimSpace(string(raw))
switch {
case strings.HasPrefix(v, "{"):
var nested map[string]json.RawMessage
if json.Unmarshal(raw, &nested) == nil && len(nested) > 0 {
out := []string{indent + key + ":"}
nkeys := make([]string, 0, len(nested))
for k := range nested {
nkeys = append(nkeys, k)
}
sort.Strings(nkeys)
for _, nk := range nkeys {
out = append(out, jsonFieldLines(nk, nested[nk], depth+1)...)
}
return out
}
case strings.HasPrefix(v, "["):
var items []json.RawMessage
if json.Unmarshal(raw, &items) == nil {
out := []string{indent + key + ": (" + itoa(len(items)) + ")"}
for _, it := range items {
out = append(out, indent+" • "+scalarString(it))
}
return out
}
}
return []string{indent + key + ": " + scalarString(raw)}
}
// scalarString renders a JSON scalar (or compact fallback) as a single-line human string.
func scalarString(raw json.RawMessage) string {
if s := jsonString(raw); s != "" {
return firstLine(s)
}
return firstLine(strings.TrimSpace(string(raw)))
}
func jsonString(raw json.RawMessage) string {
if len(raw) == 0 {
return ""
}
var s string
if json.Unmarshal(raw, &s) == nil {
return s
}
return ""
}
func firstLine(s string) string {
s = strings.TrimSpace(s)
if i := strings.IndexByte(s, '\n'); i >= 0 {
return strings.TrimSpace(s[:i]) + " …"
}
return s
}