@@ -109,8 +109,8 @@ func TestSubagentLogNesting(t *testing.T) {
109109 }
110110}
111111
112- // TestRenderStepsSubagentAndError exercises the enriched step rendering : a
113- // sub-agent label, a nested log tree , and an error-tinted result.
112+ // TestRenderStepsSubagentAndError exercises the one-line step summary : a
113+ // sub-agent label, a compact result arrow , and an error-tinted result.
114114func TestRenderStepsSubagentAndError (t * testing.T ) {
115115 m := newTestModel ()
116116 msg := message {
@@ -123,21 +123,155 @@ func TestRenderStepsSubagentAndError(t *testing.T) {
123123 result : "exit status 1\n FAIL" },
124124 },
125125 }
126- out := plain (m .renderSteps (msg ))
127- for _ , want := range []string {"sub-agent" , "⎿" , "explorer" , "✗" , "exit status 1" } {
128- if ! strings .Contains (out , want ) {
129- t .Errorf ("renderSteps missing %q in:\n %s" , want , out )
126+ out , _ := m .renderSteps (msg , 0 , 0 )
127+ plainOut := plain (out )
128+ for _ , want := range []string {"sub-agent" , "delegate_task" , "explore" , "→ done" , "✗" , "shell" , "go test" , "→ exit status 1" , "▶" } {
129+ if ! strings .Contains (plainOut , want ) {
130+ t .Errorf ("renderSteps missing %q in:\n %s" , want , plainOut )
130131 }
131132 }
133+ // Nested logs are still captured but no longer expanded in the default render.
134+ if strings .Contains (plainOut , "⎿" ) || strings .Contains (plainOut , "explorer" ) {
135+ t .Errorf ("renderSteps should not expand nested logs in one-line mode:\n %s" , plainOut )
136+ }
132137
133138 // Streaming turn: a not-done step renders the live spinner; a not-done step
134139 // in a finalized turn renders the pending glyph. Also drive the narrow-width
135140 // budget floor.
136141 m .vp .Width = 8
137- if s := m .renderSteps (message {streaming : true , steps : []step {{name : "read" , arg : "x" }}}); s == "" {
142+ if s , _ := m .renderSteps (message {streaming : true , steps : []step {{name : "read" , arg : "x" }}}, 0 , 0 ); s == "" {
138143 t .Error ("streaming step rendered empty" )
139144 }
140- if s := m .renderSteps (message {streaming : false , steps : []step {{name : "read" }}}); ! strings .Contains (plain (s ), "▸" ) {
145+ if s , _ := m .renderSteps (message {streaming : false , steps : []step {{name : "read" }}}, 0 , 0 ); ! strings .Contains (plain (s ), "▸" ) {
141146 t .Errorf ("pending step missing ▸ glyph: %q" , plain (s ))
142147 }
143148}
149+
150+ func TestRenderStepsExpanded (t * testing.T ) {
151+ m := newTestModel ()
152+ msg := message {role : roleAsst , streaming : false , steps : []step {
153+ {name : "shell" , arg : "go test" , done : true , isErr : true , expanded : true ,
154+ result : "exit status 1\n FAIL" },
155+ }}
156+ out , refs := m .renderSteps (msg , 5 , 0 )
157+ plainOut := plain (out )
158+ for _ , want := range []string {"▼" , "shell" , "go test" , "exit status 1" , "FAIL" } {
159+ if ! strings .Contains (plainOut , want ) {
160+ t .Errorf ("expanded step missing %q in:\n %s" , want , plainOut )
161+ }
162+ }
163+ if strings .Contains (plainOut , "▶" ) {
164+ t .Errorf ("expanded step should not show ▶ in:\n %s" , plainOut )
165+ }
166+ if len (refs ) != 1 || refs [0 ].line != 5 {
167+ t .Errorf ("unexpected step refs: %+v" , refs )
168+ }
169+ }
170+
171+ func TestResultOneLiner (t * testing.T ) {
172+ if got := resultOneLiner ("first\n second" , 10 ); got != "first" {
173+ t .Errorf ("resultOneLiner first line: %q" , got )
174+ }
175+ if got := resultOneLiner ("\n \n second" , 8 ); got != "second" {
176+ t .Errorf ("resultOneLiner skips blanks: %q" , got )
177+ }
178+ if got := resultOneLiner ("" , 8 ); got != "" {
179+ t .Errorf ("resultOneLiner empty: %q" , got )
180+ }
181+ if got := resultOneLiner ("a very long first meaningful line" , 10 ); got != "a very lo…" {
182+ t .Errorf ("resultOneLiner truncation: %q" , got )
183+ }
184+ }
185+
186+ func TestToggleStep (t * testing.T ) {
187+ m := newTestModel ()
188+ m .msgs = append (m .msgs , message {role : roleAsst , steps : []step {
189+ {name : "shell" , arg : "go test" , done : true , result : "exit status 1\n FAIL" },
190+ }})
191+ m .toggleStep (0 , 0 )
192+ if ! m .msgs [0 ].steps [0 ].expanded {
193+ t .Error ("toggleStep did not expand the step" )
194+ }
195+ if m .convCount != - 1 {
196+ t .Error ("toggleStep did not invalidate the transcript cache" )
197+ }
198+ out := plain (m .conversation ())
199+ if ! strings .Contains (out , "▼" ) || ! strings .Contains (out , "FAIL" ) {
200+ t .Errorf ("expanded step not rendered:\n %s" , out )
201+ }
202+ }
203+
204+ func TestToggleRecentStep (t * testing.T ) {
205+ m := newTestModel ()
206+ m .msgs = append (m .msgs ,
207+ message {role : roleAsst , steps : []step {{name : "read" , done : true , result : "ok" }}},
208+ message {role : roleAsst , steps : []step {{name : "shell" , done : true , result : "done" }}},
209+ )
210+ m .toggleRecentStep ()
211+ if ! m .msgs [1 ].steps [0 ].expanded {
212+ t .Error ("toggleRecentStep did not expand the most recent step" )
213+ }
214+ if m .msgs [0 ].steps [0 ].expanded {
215+ t .Error ("toggleRecentStep expanded the wrong step" )
216+ }
217+ }
218+
219+ func TestKeyCtrlEExpandsStep (t * testing.T ) {
220+ m := newTestModel ()
221+ m .ta .Focus ()
222+ m .msgs = append (m .msgs , message {role : roleAsst , steps : []step {{name : "shell" , done : true , result : "ok" }}})
223+
224+ // Pressing Ctrl+E should expand the most recent step, even while typing.
225+ m .ta .SetValue ("hello" )
226+ m .Update (key ("ctrl+e" ))
227+ if ! m .msgs [0 ].steps [0 ].expanded {
228+ t .Error ("Ctrl+E should expand the recent step" )
229+ }
230+ if got := m .ta .Value (); got != "hello" {
231+ t .Errorf ("Ctrl+E should not alter the input, got %q" , got )
232+ }
233+ }
234+
235+ func TestKeyETypesLetter (t * testing.T ) {
236+ m := newTestModel ()
237+ m .ta .Focus ()
238+
239+ // The plain 'e' key must always type the letter, even at the start.
240+ m .Update (key ("e" ))
241+ if got := m .ta .Value (); got != "e" {
242+ t .Errorf ("pressing 'e' should type the letter, got %q" , got )
243+ }
244+ m .Update (key ("e" ))
245+ if got := m .ta .Value (); got != "ee" {
246+ t .Errorf ("pressing 'e' again should append the letter, got %q" , got )
247+ }
248+ }
249+
250+ func TestStepLineIndex (t * testing.T ) {
251+ m := newTestModel ()
252+ m .msgs = append (m .msgs ,
253+ message {role : roleAsst , steps : []step {{name : "read" , done : true , result : "ok" }}},
254+ message {role : roleAsst , steps : []step {{name : "shell" , done : true , result : "done" }}},
255+ )
256+ _ = m .conversation ()
257+ if len (m .stepLineIndex ) != 2 {
258+ t .Fatalf ("expected 2 step refs, got %+v" , m .stepLineIndex )
259+ }
260+ if m .stepLineIndex [0 ].line >= m .stepLineIndex [1 ].line {
261+ t .Errorf ("step refs not in ascending order: %+v" , m .stepLineIndex )
262+ }
263+ msgIdx , stepIdx , ok := m .stepAtLine (m .stepLineIndex [0 ].line )
264+ if ! ok || msgIdx != 0 || stepIdx != 0 {
265+ t .Errorf ("stepAtLine first header: got %d,%d,%v" , msgIdx , stepIdx , ok )
266+ }
267+ msgIdx , stepIdx , ok = m .stepAtLine (m .stepLineIndex [1 ].line )
268+ if ! ok || msgIdx != 1 || stepIdx != 0 {
269+ t .Errorf ("stepAtLine second header: got %d,%d,%v" , msgIdx , stepIdx , ok )
270+ }
271+ // A line between the two headers still maps to the first step.
272+ mid := (m .stepLineIndex [0 ].line + m .stepLineIndex [1 ].line ) / 2
273+ msgIdx , stepIdx , ok = m .stepAtLine (mid )
274+ if ! ok || msgIdx != 0 || stepIdx != 0 {
275+ t .Errorf ("stepAtLine between headers: got %d,%d,%v" , msgIdx , stepIdx , ok )
276+ }
277+ }
0 commit comments