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 }