detached aed6edb

fix: wrap expanded compaction summaries

commit
aed6edbacb715c5139d3a30ecb349ea6496e620b
author
patriceckhart · · 2026-09-17 13:15 UTC
parent
5f8ce64
signature
unsigned
refs
tag: v0.3.78
3 files · +61 -5
M README.md +1 -1
@@ -450,7 +450,7 @@ Opens a picker listing every discovered SKILL.md file, built-ins hidden. Each ro
450450
451451 ### `/compact`
452452
453-Sends the current transcript through the model with a structured summarization prompt. The returned summary replaces the transcript as one synthetic user message, with the last few exchanges kept verbatim for continuity. The status bar's context meter resets. Use it when the context meter creeps past ~80%.
453+Sends the current transcript through the model with a structured summarization prompt. The returned summary replaces the transcript as one synthetic user message, with the last few exchanges kept verbatim for continuity. The status bar's context meter resets. Use it when the context meter creeps past ~80%. Press `ctrl+o` to expand the summary. Expanded text wraps to the current terminal width, including after resizing.
454454
455455 zot also auto-compacts in the background: after any turn that reaches the configured context threshold, the agent kicks off a condense pass on its own. Choose `off`, `70%`, `80%`, `85%` (default), or `90%` under `/settings` → **auto-compact threshold**. You'll see `condensing history, esc to cancel` above the status bar and an `(auto)` tag next to the context percentage; `esc` aborts it without touching the transcript. Turning the percentage trigger off does not disable automatic compaction and retry after a context-window or payload-too-large response.
456456
@@ -450,7 +450,7 @@ Opens a picker listing every discovered SKILL.md file, built-ins hidden. Each ro
450 450
451 ### `/compact`451 ### `/compact`
452 452
453-Sends the current transcript through the model with a structured summarization prompt. The returned summary replaces the transcript as one synthetic user message, with the last few exchanges kept verbatim for continuity. The status bar's context meter resets. Use it when the context meter creeps past ~80%.453+Sends the current transcript through the model with a structured summarization prompt. The returned summary replaces the transcript as one synthetic user message, with the last few exchanges kept verbatim for continuity. The status bar's context meter resets. Use it when the context meter creeps past ~80%. Press `ctrl+o` to expand the summary. Expanded text wraps to the current terminal width, including after resizing.
454 454
455 zot also auto-compacts in the background: after any turn that reaches the configured context threshold, the agent kicks off a condense pass on its own. Choose `off`, `70%`, `80%`, `85%` (default), or `90%` under `/settings` → **auto-compact threshold**. You'll see `condensing history, esc to cancel` above the status bar and an `(auto)` tag next to the context percentage; `esc` aborts it without touching the transcript. Turning the percentage trigger off does not disable automatic compaction and retry after a context-window or payload-too-large response.455 zot also auto-compacts in the background: after any turn that reaches the configured context threshold, the agent kicks off a condense pass on its own. Choose `off`, `70%`, `80%`, `85%` (default), or `90%` under `/settings` → **auto-compact threshold**. You'll see `condensing history, esc to cancel` above the status bar and an `(auto)` tag next to the context percentage; `esc` aborts it without touching the transcript. Turning the percentage trigger off does not disable automatic compaction and retry after a context-window or payload-too-large response.
456 456
M packages/tui/view.go +13 -4
@@ -2454,7 +2454,12 @@ func truncateLines(s string, n int) string {
24542454 // shows the full summary text.
24552455 func (v *View) renderCompactionBlock(m provider.Message, width int) []string {
24562456 th := v.Theme
2457- const indent = " "
2457+ indent := " "
2458+ // Leave room for a wide Unicode character on very narrow terminals.
2459+ if width < len(indent)+2 {
2460+ indent = ""
2461+ }
2462+ inner := max(1, width-len(indent))
24582463
24592464 tokens := m.Meta["tokens_before"]
24602465 if tokens == "" {
@@ -2464,7 +2469,9 @@ func (v *View) renderCompactionBlock(m provider.Message, width int) []string {
24642469 if v.ExpandAll {
24652470 var lines []string
24662471 header := th.FG256(th.Muted, fmt.Sprintf("compacted from ~%s tokens", tokens))
2467- lines = append(lines, indent+header)
2472+ for _, line := range wrapANSILine(header, inner) {
2473+ lines = append(lines, indent+line)
2474+ }
24682475 lines = append(lines, "")
24692476 for _, c := range m.Content {
24702477 if tb, ok := c.(provider.TextBlock); ok {
@@ -2472,12 +2479,14 @@ func (v *View) renderCompactionBlock(m provider.Message, width int) []string {
24722479 if idx := strings.Index(text, "\n\n"); idx >= 0 && strings.HasPrefix(text, "## Context Summary") {
24732480 text = text[idx+2:]
24742481 }
2475- md := RenderMarkdown(text, th, width-4)
2482+ md := RenderMarkdown(text, th, inner)
24762483 for _, l := range strings.Split(md, "\n") {
24772484 if len(l) > 0 && l[0] == FlushLeftSentinel {
24782485 l = l[1:]
24792486 }
2480- lines = append(lines, indent+l)
2487+ for _, line := range wrapANSILine(l, inner) {
2488+ lines = append(lines, indent+line)
2489+ }
24812490 }
24822491 }
24832492 }
@@ -2454,7 +2454,12 @@ func truncateLines(s string, n int) string {
2454 // shows the full summary text.2454 // shows the full summary text.
2455 func (v *View) renderCompactionBlock(m provider.Message, width int) []string {2455 func (v *View) renderCompactionBlock(m provider.Message, width int) []string {
2456 th := v.Theme2456 th := v.Theme
2457- const indent = " "2457+ indent := " "
2458+ // Leave room for a wide Unicode character on very narrow terminals.
2459+ if width < len(indent)+2 {
2460+ indent = ""
2461+ }
2462+ inner := max(1, width-len(indent))
2458 2463
2459 tokens := m.Meta["tokens_before"]2464 tokens := m.Meta["tokens_before"]
2460 if tokens == "" {2465 if tokens == "" {
@@ -2464,7 +2469,9 @@ func (v *View) renderCompactionBlock(m provider.Message, width int) []string {
2464 if v.ExpandAll {2469 if v.ExpandAll {
2465 var lines []string2470 var lines []string
2466 header := th.FG256(th.Muted, fmt.Sprintf("compacted from ~%s tokens", tokens))2471 header := th.FG256(th.Muted, fmt.Sprintf("compacted from ~%s tokens", tokens))
2467- lines = append(lines, indent+header)2472+ for _, line := range wrapANSILine(header, inner) {
2473+ lines = append(lines, indent+line)
2474+ }
2468 lines = append(lines, "")2475 lines = append(lines, "")
2469 for _, c := range m.Content {2476 for _, c := range m.Content {
2470 if tb, ok := c.(provider.TextBlock); ok {2477 if tb, ok := c.(provider.TextBlock); ok {
@@ -2472,12 +2479,14 @@ func (v *View) renderCompactionBlock(m provider.Message, width int) []string {
2472 if idx := strings.Index(text, "\n\n"); idx >= 0 && strings.HasPrefix(text, "## Context Summary") {2479 if idx := strings.Index(text, "\n\n"); idx >= 0 && strings.HasPrefix(text, "## Context Summary") {
2473 text = text[idx+2:]2480 text = text[idx+2:]
2474 }2481 }
2475- md := RenderMarkdown(text, th, width-4)2482+ md := RenderMarkdown(text, th, inner)
2476 for _, l := range strings.Split(md, "\n") {2483 for _, l := range strings.Split(md, "\n") {
2477 if len(l) > 0 && l[0] == FlushLeftSentinel {2484 if len(l) > 0 && l[0] == FlushLeftSentinel {
2478 l = l[1:]2485 l = l[1:]
2479 }2486 }
2480- lines = append(lines, indent+l)2487+ for _, line := range wrapANSILine(l, inner) {
2488+ lines = append(lines, indent+line)
2489+ }
2481 }2490 }
2482 }2491 }
2483 }2492 }
A packages/tui/view_compaction_test.go +47 -0
@@ -0,0 +1,47 @@
1+package tui
2+
3+import (
4+ "strings"
5+ "testing"
6+
7+ "github.com/patriceckhart/zot/packages/provider"
8+)
9+
10+func TestExpandedCompactionWrapsAfterResize(t *testing.T) {
11+ for _, text := range []string{
12+ strings.Repeat("summary words ", 20),
13+ "**" + strings.Repeat("styled text ", 20) + "**",
14+ "```go\n" + strings.Repeat("longIdentifier", 20) + "\n```",
15+ strings.Repeat("日本語 Grüße ", 20),
16+ } {
17+ v := View{
18+ Theme: Dark,
19+ Messages: []provider.Message{{
20+ Role: provider.RoleUser,
21+ Meta: map[string]string{"compaction": "true", "tokens_before": "12345"},
22+ Content: []provider.Content{provider.TextBlock{
23+ Text: "## Context Summary\n\n" + text,
24+ }},
25+ }},
26+ }
27+ if got := strings.TrimSpace(strings.Join(v.Build(120), "")); got != "" {
28+ t.Fatalf("collapsed summary should be hidden: %q", got)
29+ }
30+ v.ExpandAll = true
31+ canonical := func(s string) string {
32+ return strings.Join(strings.Fields(stripANSI(s)), "")
33+ }
34+ want := canonical("compacted from ~12345 tokens" + RenderMarkdown(text, Dark, 116))
35+ for _, width := range []int{120, 40, 12, 4, 2, 120} {
36+ lines := v.Build(width)
37+ for _, line := range lines {
38+ if got := visibleWidth(line); got > width {
39+ t.Fatalf("row width %d exceeds terminal width %d: %q", got, width, stripANSI(line))
40+ }
41+ }
42+ if got := canonical(strings.Join(lines, "")); got != want {
43+ t.Fatalf("width %d lost summary content\ngot: %s\nwant: %s", width, got, want)
44+ }
45+ }
46+ }
47+}
@@ -0,0 +1,47 @@
1+package tui
2+
3+import (
4+ "strings"
5+ "testing"
6+
7+ "github.com/patriceckhart/zot/packages/provider"
8+)
9+
10+func TestExpandedCompactionWrapsAfterResize(t *testing.T) {
11+ for _, text := range []string{
12+ strings.Repeat("summary words ", 20),
13+ "**" + strings.Repeat("styled text ", 20) + "**",
14+ "```go\n" + strings.Repeat("longIdentifier", 20) + "\n```",
15+ strings.Repeat("日本語 Grüße ", 20),
16+ } {
17+ v := View{
18+ Theme: Dark,
19+ Messages: []provider.Message{{
20+ Role: provider.RoleUser,
21+ Meta: map[string]string{"compaction": "true", "tokens_before": "12345"},
22+ Content: []provider.Content{provider.TextBlock{
23+ Text: "## Context Summary\n\n" + text,
24+ }},
25+ }},
26+ }
27+ if got := strings.TrimSpace(strings.Join(v.Build(120), "")); got != "" {
28+ t.Fatalf("collapsed summary should be hidden: %q", got)
29+ }
30+ v.ExpandAll = true
31+ canonical := func(s string) string {
32+ return strings.Join(strings.Fields(stripANSI(s)), "")
33+ }
34+ want := canonical("compacted from ~12345 tokens" + RenderMarkdown(text, Dark, 116))
35+ for _, width := range []int{120, 40, 12, 4, 2, 120} {
36+ lines := v.Build(width)
37+ for _, line := range lines {
38+ if got := visibleWidth(line); got > width {
39+ t.Fatalf("row width %d exceeds terminal width %d: %q", got, width, stripANSI(line))
40+ }
41+ }
42+ if got := canonical(strings.Join(lines, "")); got != want {
43+ t.Fatalf("width %d lost summary content\ngot: %s\nwant: %s", width, got, want)
44+ }
45+ }
46+ }
47+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close