feat: tighten freestyle discovery and analyst handoff
This commit is contained in:
@@ -825,21 +825,40 @@ func (m Model) buildTranscriptRows(w int) ([]string, []int) {
|
||||
if e.Reasoning != "" {
|
||||
if m.thinkingShown {
|
||||
for _, ln := range strings.Split(strings.TrimRight(e.Reasoning, "\n"), "\n") {
|
||||
rows = append(rows, t.span("✼ "+ln, t.P.Faint))
|
||||
rows = append(rows, t.span(" ✼ "+ln, t.P.Faint))
|
||||
}
|
||||
} else {
|
||||
rows = append(rows, t.span("✼ reasoning — palette: thinking", t.P.Faint))
|
||||
rows = append(rows, t.span(" ✼ reasoning — palette: thinking", t.P.Faint))
|
||||
}
|
||||
} else if m.lastReasoning != "" {
|
||||
if m.thinkingShown {
|
||||
for _, ln := range strings.Split(strings.TrimRight(m.lastReasoning, "\n"), "\n") {
|
||||
rows = append(rows, t.span("✼ "+ln, t.P.Faint))
|
||||
rows = append(rows, t.span(" ✼ "+ln, t.P.Faint))
|
||||
}
|
||||
} else {
|
||||
rows = append(rows, t.span("✼ reasoning — palette: thinking", t.P.Faint))
|
||||
rows = append(rows, t.span(" ✼ reasoning — palette: thinking", t.P.Faint))
|
||||
}
|
||||
}
|
||||
gutter := t.span(" ┈", t.P.Faint)
|
||||
summary := toolRowSummary(e.Content)
|
||||
avail := w - 4
|
||||
if avail < 10 {
|
||||
avail = 10
|
||||
}
|
||||
prefixW := lipgloss.Width(gutter) + 1
|
||||
contentW := avail - prefixW
|
||||
if contentW < 4 {
|
||||
contentW = 4
|
||||
}
|
||||
parts := wrapContent(summary, contentW)
|
||||
for i, ln := range parts {
|
||||
if i == 0 {
|
||||
rows = append(rows, gutter+" "+t.span(ln, t.P.Dim))
|
||||
} else {
|
||||
indent := t.span(strings.Repeat(" ", prefixW+1), t.P.Bg)
|
||||
rows = append(rows, indent+t.span(ln, t.P.Dim))
|
||||
}
|
||||
}
|
||||
rows = append(rows, t.span(toolRowSummary(e.Content), t.P.Dim))
|
||||
case "thinking":
|
||||
// Collapsed by default to a single muted line so the reasoning trace doesn't bury
|
||||
// the answer; the palette "thinking" toggle reveals the full dimmed block.
|
||||
@@ -849,12 +868,13 @@ func (m Model) buildTranscriptRows(w int) ([]string, []int) {
|
||||
rows = append(rows, brain+t.span(" thinking ("+plural(n, "line")+") — palette: thinking", t.P.Faint))
|
||||
break
|
||||
}
|
||||
lines := wrap(e.Content, w-2)
|
||||
rendered := renderMarkdown(e.Content, w-2)
|
||||
lines := strings.Split(rendered, "\n")
|
||||
for i, ln := range lines {
|
||||
if i == 0 {
|
||||
rows = append(rows, brain+t.span(" ", t.P.Bg)+t.span(ln, t.P.Faint))
|
||||
rows = append(rows, brain+t.span(" ", t.P.Bg)+lipgloss.NewStyle().Foreground(t.P.Faint).Background(t.P.Bg).Render(ln))
|
||||
} else {
|
||||
rows = append(rows, t.span(" ", t.P.Bg)+t.span(ln, t.P.Faint))
|
||||
rows = append(rows, t.span(" ", t.P.Bg)+lipgloss.NewStyle().Foreground(t.P.Faint).Background(t.P.Bg).Render(ln))
|
||||
}
|
||||
}
|
||||
case "narration_llm":
|
||||
@@ -878,8 +898,35 @@ func (m Model) buildTranscriptRows(w int) ([]string, []int) {
|
||||
if m.actionsHidden {
|
||||
break
|
||||
}
|
||||
icon := lipgloss.NewStyle().Foreground(t.P.Accent2).Background(t.P.Bg).Render(e.Icon)
|
||||
rows = append(rows, t.span(" ", t.P.Bg)+icon+t.span(" ", t.P.Bg)+t.span(e.Content, t.P.Dim))
|
||||
iconFg := t.P.Accent2
|
||||
switch e.Icon {
|
||||
case "✓":
|
||||
iconFg = t.P.OK
|
||||
case "✗":
|
||||
iconFg = t.P.Bad
|
||||
case "✕":
|
||||
iconFg = t.P.Warn
|
||||
}
|
||||
gutter := t.span(" ┈", t.P.Faint)
|
||||
icon := lipgloss.NewStyle().Foreground(iconFg).Background(t.P.Bg).Bold(true).Render(e.Icon)
|
||||
prefix := gutter + " " + icon + " "
|
||||
avail := w - 4 // box inner padding
|
||||
if avail < 10 {
|
||||
avail = 10
|
||||
}
|
||||
contentW := avail - lipgloss.Width(prefix)
|
||||
if contentW < 4 {
|
||||
contentW = 4
|
||||
}
|
||||
parts := wrapContent(e.Content, contentW)
|
||||
for i, ln := range parts {
|
||||
if i == 0 {
|
||||
rows = append(rows, prefix+t.span(ln, t.P.FgStrong))
|
||||
} else {
|
||||
indent := t.span(strings.Repeat(" ", lipgloss.Width(prefix)), t.P.Bg)
|
||||
rows = append(rows, indent+t.span(ln, t.P.FgStrong))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return rows, msgStart
|
||||
@@ -1171,16 +1218,18 @@ func itoa(n int) string {
|
||||
return string(b[i:])
|
||||
}
|
||||
|
||||
// toolRowSummary collapses a tool-output transcript entry into a one-line pointer. A
|
||||
// write/edit entry holds a unified diff, so summarise it by what ^x actually shows — the
|
||||
// diff's row count — instead of the raw diff byte length (which read as a meaningless
|
||||
// "N chars": it counted +/-/@@/header bytes, not anything the operator cares about, and
|
||||
// len() is bytes not characters). Non-diff output falls back to a true character count.
|
||||
// toolRowSummary collapses a tool-output transcript entry into a one-line summary. For
|
||||
// write/edit entries (unified diffs) it shows the file path and row count; non-diff output
|
||||
// shows a character count. The leading badge (▾ diff / ▾ output) tells the kind at a glance.
|
||||
func toolRowSummary(content string) string {
|
||||
if isUnifiedDiff(content) {
|
||||
return "· diff (" + itoa(previewRowCount(content)) + " rows) — ^x to view"
|
||||
n := itoa(previewRowCount(content))
|
||||
if p := diffTarget(content); p != "" {
|
||||
return "▾ diff · " + p + " · " + n + " rows · ^x"
|
||||
}
|
||||
return "▾ diff · " + n + " rows · ^x"
|
||||
}
|
||||
return "· tool output (" + itoa(len([]rune(content))) + " chars) — ^x to view"
|
||||
return "▾ output · " + itoa(len([]rune(content))) + " chars · ^x"
|
||||
}
|
||||
|
||||
// metricsSuffix formats the faint latency+token annotation for a ROUTER turn.
|
||||
@@ -1217,3 +1266,55 @@ func wrap(s string, w int) []string {
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
// wrapContent splits plain text into lines no wider than w. Words that fit whole
|
||||
// are kept together; a word longer than w is character-wrapped at w. Returns at
|
||||
// least one line.
|
||||
func wrapContent(s string, w int) []string {
|
||||
if w < 1 {
|
||||
w = 1
|
||||
}
|
||||
if len(s) <= w {
|
||||
return []string{s}
|
||||
}
|
||||
words := strings.Fields(s)
|
||||
if len(words) == 0 {
|
||||
return []string{""}
|
||||
}
|
||||
var lines []string
|
||||
cur := ""
|
||||
flush := func() {
|
||||
if cur != "" {
|
||||
lines = append(lines, cur)
|
||||
cur = ""
|
||||
}
|
||||
}
|
||||
for _, word := range words {
|
||||
// Does the word itself overflow the width? If so, flush current line,
|
||||
// then character-wrap the word.
|
||||
if len(word) > w {
|
||||
flush()
|
||||
for len(word) > w {
|
||||
lines = append(lines, word[:w])
|
||||
word = word[w:]
|
||||
}
|
||||
if word != "" {
|
||||
cur = word
|
||||
}
|
||||
continue
|
||||
}
|
||||
if cur == "" {
|
||||
cur = word
|
||||
} else if len(cur)+1+len(word) <= w {
|
||||
cur += " " + word
|
||||
} else {
|
||||
lines = append(lines, cur)
|
||||
cur = word
|
||||
}
|
||||
}
|
||||
flush()
|
||||
if len(lines) == 0 {
|
||||
return []string{""}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user