Skip to content

Commit bfa2b5a

Browse files
author
gluck
committed
the rule stops repeating the aria, j/k choose, and a result is an alert
FOUR OF YOURS, and the rest are listed below as not-yet. THE RULE NO LONGER CARRIES THE ARIA ID. It moved onto the bar last commit, and the rule kept printing it one row above -- the whole width of a line spent saying what the row below already said. What is left is the only thing the rule knows that the bar does not: where this window sits in the conversation, and whether it is following. With no position it is now a plain rule. THE PIT'S NAME IS NOT A VERBOSE EXTRA. The design draws "𝄚 queue", always; `token()` was hiding the word behind ^V, so the succinct bar said "𝄚" -- an answer for someone who has already memorised the glyphs and for nobody else. The queue's title row went with it: the pit says what it is on the bar, and a title repeated the same word one line above at the cost of a row. THE INCIPIT SHOWS WHAT THE PAGER SHOWS. bookendLines forced verbose on the theory that scrollback cannot be asked to toggle -- so turning detail OFF in the pager left the inline stream showing it anyway. "One canonical implementation" is not satisfied by one function with two callers passing different arguments. If `m` is off, it is off everywhere. AND THE OLD RENDERER IS DELETED. statusLine/statusLineVerbose were dead in production the moment statusview.go landed, and alive only in tests -- two renderers with the assertions pointed at the wrong one, which is exactly how the two modes drifted apart to begin with. j/k CHOOSE, e/y READ, u/d PAGE. Selection and scrolling were one motion and that made `form show` unusable in the other direction too: j moved the window and left the selection behind. Now j/k (and ^N/^P) move the selection with the window following, `e` scrolls a row down, `y` scrolls a row up where there is nothing to select (it is still yank where there is), u/d take half a page. A ONE-LINE RESULT IS AN ALERT, NOT A PIT. "sent" now lands in the bar's first slot -- left of the pit glyph, left of the state -- and retires after cli.notice_ttl. It used to open a message pit, which REPLACED an open queue, so the next poll rebuilt the list and an `x` aimed at row two dropped row one. Anything longer than half a row is still a pit, because a bar row cannot hold it. STILL OUTSTANDING, and I would rather name them than let them look done: `form show` has no glyph (only `form listen` is a live view; show takes another path), the pit's height still moves by a row when a selection appears, and fullscreen (F to shadow the transcript) is not started.
1 parent b8e1a42 commit bfa2b5a

12 files changed

Lines changed: 166 additions & 141 deletions

‎internal/cli/drawerid.go‎

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -104,19 +104,25 @@ func (d drawerID) face() drawerFace {
104104
// a keyMode from what is on screen.
105105
func (d drawerID) keys() keyMode { return d.face().keys }
106106

107-
// token is what the drawer contributes to the left of the status bar: its
108-
// glyph, plus its name under verbose. Empty for the drawers that have nothing
109-
// to announce, so the row then begins with the state.
110-
func (d drawerID) token(verbose bool) string {
107+
// token is what the pit contributes to the left of the status bar: GLYPH AND
108+
// NAME, always, because that is what the design draws --
109+
//
110+
// 𝄚 queue · ✓ · 123abc · test 9.8k/1.0m (1.0%)
111+
//
112+
// It is not a verbose extra. The name is how you know which pit you are in,
113+
// and hiding it behind a toggle made the succinct bar say only "𝄚", which
114+
// answers the question for someone who has already memorised the glyphs and
115+
// for nobody else. Empty for the pits with nothing to announce, so the row
116+
// then begins with the state.
117+
func (d drawerID) token() string {
111118
f := d.face()
112-
switch {
113-
case f.glyph == "":
119+
if f.glyph == "" {
114120
return ""
115-
case verbose && f.name != "":
116-
return f.glyph + " " + f.name
117-
default:
121+
}
122+
if f.name == "" {
118123
return f.glyph
119124
}
125+
return f.glyph + " " + f.name
120126
}
121127

122128
// selectionGlyph marks the row under the cursor. It is PER DRAWER, not one

‎internal/cli/keymap_equiv_test.go‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,11 @@ var pagerOracle = []struct {
204204
keys map[string]string
205205
}{
206206
{"help", "off:same fol=true srch=false q=\"\" mq=\"\" h=false s=false Q=false g=false sel=false exp=0 jmp=false jq=\"\"", map[string]string{
207+
// `e` READS: it scrolls the pit by a row without moving the selection
208+
// (vim's ^E, unmodified, because the chord is spent on selection).
209+
// j/k now CHOOSE, u/d page, and y scrolls back only where there is
210+
// nothing to select -- otherwise y is still yank.
211+
"0x65": "off:bottom fol=true srch=false q=\"\" mq=\"\" h=true s=false Q=false g=false sel=false exp=0 jmp=false jq=\"\"",
207212
// REGENERATED for THE PICKER (picker.go): every drawer now owns the
208213
// list motions -- j/k, u/d, gg/G and the arrow cluster move whichever
209214
// drawer is open instead of wiping it and acting on the transcript
@@ -358,6 +363,11 @@ var pagerOracle = []struct {
358363
"0x7f": "off:same fol=true srch=false q=\"\" mq=\"\" h=false s=false Q=false g=false sel=false exp=0 jmp=true jq=\"1\"",
359364
}},
360365
{"panel+sel", "off:same fol=false srch=false q=\"\" mq=\"\" h=false s=false Q=false g=false sel=true exp=0 jmp=false jq=\"\"", map[string]string{
366+
// `e` READS: it scrolls the pit by a row without moving the selection
367+
// (vim's ^E, unmodified, because the chord is spent on selection).
368+
// j/k now CHOOSE, u/d page, and y scrolls back only where there is
369+
// nothing to select -- otherwise y is still yank.
370+
"0x65": "off:same fol=false srch=false q=\"\" mq=\"\" h=false s=true Q=false g=false sel=true exp=0 jmp=false jq=\"\"",
361371
// REGENERATED for THE PICKER (picker.go): every drawer now owns the
362372
// list motions -- j/k, u/d, gg/G and the arrow cluster move whichever
363373
// drawer is open instead of wiping it and acting on the transcript
@@ -386,6 +396,11 @@ var pagerOracle = []struct {
386396
"0x78": "off:same fol=false srch=false q=\"\" mq=\"\" h=false s=true Q=false g=false sel=true exp=0 jmp=false jq=\"\"",
387397
}},
388398
{"queued", "off:same fol=true srch=false q=\"\" mq=\"\" h=false s=false Q=false g=false sel=false exp=0 jmp=false jq=\"\"", map[string]string{
399+
// `e` READS: it scrolls the pit by a row without moving the selection
400+
// (vim's ^E, unmodified, because the chord is spent on selection).
401+
// j/k now CHOOSE, u/d page, and y scrolls back only where there is
402+
// nothing to select -- otherwise y is still yank.
403+
"0x65": "off:bottom fol=true srch=false q=\"\" mq=\"\" h=false s=false Q=true g=false sel=false exp=0 jmp=false jq=\"\"",
389404
// REGENERATED for THE PICKER (picker.go): every drawer now owns the
390405
// list motions -- j/k, u/d, gg/G and the arrow cluster move whichever
391406
// drawer is open instead of wiping it and acting on the transcript
@@ -618,6 +633,11 @@ var pagerOracle = []struct {
618633
"0x7f": "off:same fol=false srch=true q=\"\" mq=\"\" h=false s=false Q=false g=false sel=true exp=0 jmp=false jq=\"\"",
619634
}},
620635
{"status", "off:same fol=true srch=false q=\"\" mq=\"\" h=false s=false Q=false g=false sel=false exp=0 jmp=false jq=\"\"", map[string]string{
636+
// `e` READS: it scrolls the pit by a row without moving the selection
637+
// (vim's ^E, unmodified, because the chord is spent on selection).
638+
// j/k now CHOOSE, u/d page, and y scrolls back only where there is
639+
// nothing to select -- otherwise y is still yank.
640+
"0x65": "off:bottom fol=true srch=false q=\"\" mq=\"\" h=false s=true Q=false g=false sel=false exp=0 jmp=false jq=\"\"",
621641
// REGENERATED for THE PICKER (picker.go): every drawer now owns the
622642
// list motions -- j/k, u/d, gg/G and the arrow cluster move whichever
623643
// drawer is open instead of wiping it and acting on the transcript

‎internal/cli/keymap_input_equiv_test.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,11 @@ var inputOracle = []struct {
558558
"csiu ^p+alt": "stop=false rest=\"\" act=true off:same fol=true srch=false q=\"\" h=false s=false Q=false g=false sel=false verb=false disc=0 canc=false clip=\"\" cpfail=false cping=false jmp=true jq=\"12\"",
559559
}},
560560
{"panel", "stop=false rest=\"\" act=true off:same fol=true srch=false q=\"\" h=false s=false Q=false g=false sel=false verb=false disc=0 canc=false clip=\"\" cpfail=false cping=false jmp=false jq=\"\"", map[string]string{
561+
// `e` READS: it scrolls the pit by a row without moving the selection
562+
// (vim's ^E, unmodified, because the chord is spent on selection).
563+
// j/k now CHOOSE, u/d page, and y scrolls back only where there is
564+
// nothing to select -- otherwise y is still yank.
565+
"0x65": "stop=false rest=\"\" act=true off:bottom fol=true srch=false q=\"\" h=true s=false Q=false g=false sel=false verb=false disc=0 canc=false clip=\"\" cpfail=false cping=false jmp=false jq=\"\"",
561566
// REGENERATED for THE PICKER (picker.go): every drawer now owns the
562567
// list motions -- j/k, u/d, gg/G and the arrow cluster move whichever
563568
// drawer is open instead of wiping it and acting on the transcript

‎internal/cli/livelog_bridge_test.go‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
func TestIncipitFreezesAssistantAfterTerminalStatusArrives(t *testing.T) {
1515
var out bytes.Buffer
1616
status := newSessionStatus("aria1234", time.Now())
17-
bookend := func() []string { return []string{status.ruleLine(80, ""), status.statusLineVerbose(80, false, true)} }
17+
bookend := func() []string { return footerStanza(status, 80, "", drawerNothing, status.barVerbose()) }
1818
lt := newLivelogTurn(&out, 80, 20, &renderSettings{}, "aria1234", time.Now(), status, bookend, func() string { return "rule" })
1919

2020
lt.apply(aria.Page{Parts: []aria.TurnPart{{Turn: aria.Turn{ID: uint64(2), Live: &aria.Live{From: 0, V: 0, Nodes: []aria.NodeDelta{{
@@ -30,15 +30,15 @@ func TestIncipitFreezesAssistantAfterTerminalStatusArrives(t *testing.T) {
3030
if lt.pending != nil {
3131
t.Fatal("terminal status must freeze pending assistant output")
3232
}
33-
if !strings.Contains(out.String(), "done ✓") {
33+
if !strings.Contains(out.String(), "✓") {
3434
t.Fatalf("incipit did not freeze completed status:\n%s", out.String())
3535
}
3636
}
3737

3838
func TestIncipitFreezesAssistantCloseAfterTurnDone(t *testing.T) {
3939
var out bytes.Buffer
4040
status := newSessionStatus("aria1234", time.Now())
41-
bookend := func() []string { return []string{status.ruleLine(80, ""), status.statusLineVerbose(80, false, true)} }
41+
bookend := func() []string { return footerStanza(status, 80, "", drawerNothing, status.barVerbose()) }
4242
lt := newLivelogTurn(&out, 80, 20, &renderSettings{}, "aria1234", time.Now(), status, bookend, func() string { return "rule" })
4343

4444
lt.apply(aria.Page{Parts: []aria.TurnPart{{Turn: aria.Turn{ID: uint64(2), Live: &aria.Live{From: 0, V: 0, Nodes: []aria.NodeDelta{{
@@ -51,15 +51,15 @@ func TestIncipitFreezesAssistantCloseAfterTurnDone(t *testing.T) {
5151
if lt.pending != nil {
5252
t.Fatal("late assistant close must freeze after turn completion")
5353
}
54-
if !strings.Contains(out.String(), "done ✓") {
54+
if !strings.Contains(out.String(), "✓") {
5555
t.Fatalf("late close did not freeze completed status:\n%s", out.String())
5656
}
5757
}
5858

5959
func TestIncipitRepaintsDiscardedAssistantWithErrorStatus(t *testing.T) {
6060
var out bytes.Buffer
6161
status := newSessionStatus("aria1234", time.Now())
62-
bookend := func() []string { return []string{status.ruleLine(80, ""), status.statusLineVerbose(80, false, true)} }
62+
bookend := func() []string { return footerStanza(status, 80, "", drawerNothing, status.barVerbose()) }
6363
lt := newLivelogTurn(&out, 80, 20, &renderSettings{}, "aria1234", time.Now(), status, bookend, func() string { return "rule" })
6464

6565
lt.apply(aria.Page{Parts: []aria.TurnPart{{Turn: aria.Turn{ID: uint64(2), Live: &aria.Live{From: 0, V: 0, Nodes: []aria.NodeDelta{{
@@ -68,7 +68,7 @@ func TestIncipitRepaintsDiscardedAssistantWithErrorStatus(t *testing.T) {
6868
}}}}}}})
6969
lt.finishTurn("error: provider failed")
7070

71-
if !strings.Contains(out.String(), "error ✗") {
71+
if !strings.Contains(out.String(), "✗") {
7272
t.Fatalf("discarded assistant retained thinking status:\n%s", out.String())
7373
}
7474
}

‎internal/cli/session_status.go‎

Lines changed: 20 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -278,90 +278,28 @@ func (s *sessionStatus) turnLabel(verbose bool) string {
278278
// ruleLine is the upper of the two footer rows: a full-width rule with the
279279
// identity right-aligned: "─────…── aria <id>[ · <pos>] ───". Undecorated
280280
// (the caller dims it).
281+
// ruleLine closes the conversation. IT NO LONGER CARRIES THE ARIA ID: the id
282+
// moved onto the status bar, and printing it twice, one row apart, spent the
283+
// rule's whole width saying what the row below already said. What is left is
284+
// the only thing the rule knows that the bar does not -- where in the
285+
// conversation this window sits, and whether it is following.
281286
func (s *sessionStatus) ruleLine(width int, pos string) string {
282-
label := "aria " + s.figaroID
283-
if pos != "" {
284-
label += " · " + pos
287+
if pos == "" {
288+
return clipToWidth(strings.Repeat("─", max(width, 0)), width)
285289
}
286-
right := " " + label + " ───"
290+
right := " " + pos + " ───"
287291
fill := width - runewidth.StringWidth(right)
288292
if fill < 3 {
289293
fill = 3
290294
}
291295
return clipToWidth(strings.Repeat("─", fill)+right, width)
292296
}
293297

294-
// statusLine is the lower footer row: plain left-aligned text -
295-
// "<mantra> · <turn state> · ctx … · cost … · <time>[ · ? help · ! status]".
296-
// hints adds the key hooks (live pager only; frozen scrollback omits them).
297-
// Narrow panes shed the mantra first, then cost, then ctx, then the time -
298-
// the turn state and the hints survive last.
299-
func (s *sessionStatus) statusLine(width int, hints bool) string {
300-
return s.statusLineVerbose(width, hints, s.verbose)
301-
}
302-
303-
// statusLineVerbose is statusLine with the verbosity named rather than read
304-
// off the session. FROZEN ROWS PASS true: the bookend that lands in scrollback
305-
// is a permanent artifact, and `^V` cannot be pressed on a screenful of text
306-
// that scrolled past an hour ago. A live row can be asked; a dead one cannot,
307-
// so it says the word.
308-
func (s *sessionStatus) statusLineVerbose(width int, hints, verbose bool) string {
309-
if s == nil {
310-
return ""
311-
}
312-
s.mu.RLock()
313-
type tok struct {
314-
text string
315-
rank int // shed order: lower sheds first (0 = mantra)
316-
}
317-
var tokens []tok
318-
// THE NOTICE IS THE ONE TOKEN THAT MUST NOT BE SHED, and it goes first:
319-
// trouble belongs where the eye lands, not after the token cost. Rank 6 is
320-
// above every shed pass, so only the ellipsis can ever shorten it. Red is
321-
// re-lit against the caller's dim wrapper (22 = not-dim) and handed back
322-
// dim + default-foreground, so the rest of the row is unchanged.
323-
if s.notice != "" {
324-
tokens = append(tokens, tok{term.NoticeInDim(s.notice), 6})
325-
}
326-
if mantra := strings.Join(strings.Fields(s.metrics.Mantra), " "); mantra != "" {
327-
tokens = append(tokens, tok{truncRunes(mantra, 32), 0})
328-
}
329-
if label := s.turnLabel(verbose); label != "" {
330-
tokens = append(tokens, tok{label, 4})
331-
}
332-
if context := formatContextUsage(s.metrics.ContextTokens, s.metrics.ContextLimit, s.metrics.ContextExact); context != "-" {
333-
tokens = append(tokens, tok{"ctx " + context, 2})
334-
}
335-
if cost := formatSessionTokenCost(s.metrics.TokensIn, s.metrics.TokensOut); cost != "-" {
336-
tokens = append(tokens, tok{"cost " + cost, 1})
337-
}
338-
tokens = append(tokens, tok{s.startedAt.Format("15:04:05"), 3})
339-
if hints {
340-
tokens = append(tokens, tok{"? help", 5}, tok{"! status", 5})
341-
}
342-
s.mu.RUnlock()
343-
344-
join := func() string {
345-
parts := make([]string, 0, len(tokens))
346-
for _, t := range tokens {
347-
parts = append(parts, t.text)
348-
}
349-
return strings.Join(parts, " · ")
350-
}
351-
for rank := 0; rank < 4 && displayWidth(join()) > width; rank++ {
352-
kept := tokens[:0]
353-
for _, t := range tokens {
354-
if t.rank != rank {
355-
kept = append(kept, t)
356-
}
357-
}
358-
tokens = kept
359-
}
360-
// Ellipsis, not a hard clip: the status row is read as a sentence, and a
361-
// bare cut ends mid-word with nothing to say it was cut. One column buys
362-
// the difference between "cost 4.5k to" and "cost 4.5k…".
363-
return clipToWidthEllipsis(join(), width)
364-
}
298+
// THE OLD ASSEMBLED ROW IS GONE. statusLine()/statusLineVerbose() built the
299+
// bar by hand -- token list, rank ladder, ellipsis -- and after statusview.go
300+
// landed they were dead in production and alive only in tests. Two renderers,
301+
// one of them asserted against: that is how the pager and the incipit came to
302+
// disagree in the first place. The bar is statusView.render and nothing else.
365303

366304
// panelLines is the '!' status panel: the figaro-status detail rendered from
367305
// the live metrics snapshot, shown above the footer while output streams.
@@ -434,11 +372,14 @@ func formatTokenCount(tokens int) string {
434372
// footer the pager draws -- footerStanza -- with no position label, because a
435373
// stream that is not paging has no window to report.
436374
//
437-
// FROZEN ROWS ARE VERBOSE, and that is the one thing this caller decides: the
438-
// bookend lands in scrollback, where ^V cannot be pressed on text that scrolled
439-
// past an hour ago. A live bar can be asked; a dead one has to say the word.
375+
// THE INCIPIT'S BAR IS THE PAGER'S BAR. It reads the same verbosity from the
376+
// same session, because "one canonical implementation" is not satisfied by one
377+
// FUNCTION with two callers that pass different arguments -- this caller used
378+
// to force verbose on the theory that scrollback cannot be asked, and the
379+
// result was an inline stream showing detail the pager had just been told to
380+
// stop showing. If `m` is off, it is off everywhere.
440381
func bookendLines(status *sessionStatus) []string {
441-
return footerStanza(status, termWidth(), "", drawerNothing, true)
382+
return footerStanza(status, termWidth(), "", drawerNothing, status.barVerbose())
442383
}
443384

444385
// formatCtxCell renders a context size for the narrow CTX column in `list`:

‎internal/cli/session_status_notice_test.go‎

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,24 @@ func statusFixture(mantra string) *sessionStatus {
2727
return s
2828
}
2929

30+
// row renders the status bar the way the PROGRAM does: one renderer, so a test
31+
// cannot pass against a spelling production no longer uses. (statusLine, which
32+
// these tests used to call, was a second implementation of this and is gone.)
33+
func row(s *sessionStatus, w int) string {
34+
return strings.Join(s.viewOf(drawerNothing, s.barVerbose(), time.Now()).render(w), "\n")
35+
}
36+
37+
// rowWidth is the WIDEST row of the bar. The bar is one row or three, so a
38+
// width assertion must measure rows rather than the joined string -- which is
39+
// what made these tests report 56 columns for a 40-column bar.
40+
func rowWidth(s *sessionStatus, w int) int {
41+
widest := 0
42+
for _, r := range s.viewOf(drawerNothing, s.barVerbose(), time.Now()).render(w) {
43+
widest = max(widest, displayWidth(r))
44+
}
45+
return widest
46+
}
47+
3048
func TestStatusNoticeIsRedAndLeftmost(t *testing.T) {
3149
// Arm colour: a test binary's stdout is not a TTY, and the notice now
3250
// honours that like every other painted token. It did not before -- the
@@ -36,7 +54,7 @@ func TestStatusNoticeIsRedAndLeftmost(t *testing.T) {
3654

3755
s := statusFixture("a perfectly ordinary mantra")
3856
s.setNotice("error: overloaded")
39-
line := s.statusLine(200, true)
57+
line := row(s, 200)
4058

4159
// Through the palette, not a literal: the assertion asks the theme what a
4260
// notice looks like, so a palette change moves the test with the code
@@ -59,12 +77,12 @@ func TestStatusNoticeShedsLast(t *testing.T) {
5977
s := statusFixture("a perfectly ordinary mantra that is quite long indeed")
6078
s.setNotice("error: overloaded")
6179
for _, w := range []int{120, 90, 70, 50, 40, 30} {
62-
line := s.statusLine(w, true)
80+
line := row(s, w)
6381
if !strings.Contains(line, "error") {
6482
t.Errorf("width %d shed the notice: %q", w, line)
6583
}
66-
if got := displayWidth(line); got > w {
67-
t.Errorf("width %d: row is %d columns wide: %q", w, got, line)
84+
if got := rowWidth(s, w); got > w {
85+
t.Errorf("width %d: widest row is %d columns: %q", w, got, line)
6886
}
6987
}
7088
}
@@ -74,7 +92,7 @@ func TestStatusNoticeClears(t *testing.T) {
7492
s := statusFixture("m")
7593
s.setNotice("error: overloaded")
7694
s.setNotice("")
77-
if line := s.statusLine(200, true); strings.Contains(line, "overloaded") {
95+
if line := row(s, 200); strings.Contains(line, "overloaded") {
7896
t.Fatalf("notice survived being cleared: %q", line)
7997
}
8098
}
@@ -84,7 +102,7 @@ func TestStatusNoticeClears(t *testing.T) {
84102
func TestStatusNoticeIsOneLine(t *testing.T) {
85103
s := statusFixture("m")
86104
s.setNotice("first line\nsecond line\n\tthird")
87-
line := s.statusLine(200, true)
105+
line := row(s, 200)
88106
if strings.ContainsAny(line, "\n\t") {
89107
t.Fatalf("notice carried a line break into the row: %q", line)
90108
}
@@ -97,8 +115,8 @@ func TestStatusLineEllipsisOnOverflow(t *testing.T) {
97115
s := statusFixture("a mantra long enough to force the issue at narrow widths")
98116
s.setNotice("error: something went quite wrong in a long-winded way")
99117
for _, w := range []int{60, 40, 24, 12, 5, 2, 1} {
100-
line := s.statusLine(w, true)
101-
if got := displayWidth(line); got > w {
118+
line := row(s, w)
119+
if got := rowWidth(s, w); got > w {
102120
t.Fatalf("width %d: %d columns: %q", w, got, line)
103121
}
104122
if !strings.Contains(line, "…") {
@@ -110,7 +128,7 @@ func TestStatusLineEllipsisOnOverflow(t *testing.T) {
110128
// A row that fits must be left exactly alone: no ellipsis, no rewrite.
111129
func TestStatusLineNoEllipsisWhenItFits(t *testing.T) {
112130
s := statusFixture("m")
113-
line := s.statusLine(400, true)
131+
line := row(s, 400)
114132
if strings.Contains(line, "…") {
115133
t.Fatalf("a row with room to spare was ellipsised: %q", line)
116134
}

0 commit comments

Comments
 (0)