@@ -101,6 +101,12 @@ func (s *ShellItem) Complete(output string, exitCode int) {
101101 s.Bump()
102102}
103103
104+// AppendOutput appends incremental output to a pending ShellItem.
105+func (s *ShellItem) AppendOutput(chunk string) {
106+ s.output += chunk
107+ s.Bump()
108+}
109+
104110func (s *ShellItem) ID() string { return s.id }
105111func (s *ShellItem) FilterValue() string { return s.command }
106112func (s *ShellItem) Finished() bool { return !s.pending }
@@ -199,10 +205,11 @@ func (s *ShellItem) RawRender(width int) string {
199205 header := prompt + " " + highlighted
200206
201207 if s.pending {
202− return header + "\n" + s.anim.Render()
203− }
204−
205− if s.exitCode != 0 {
208+ if s.output == "" {
209+ // Nothing streamed yet: show the spinner under the header.
210+ return header + "\n" + s.anim.Render()
211+ }
212+ } else if s.exitCode != 0 {
206213 header += " " + s.sty.Messages.ShellExitCode.Render(fmt.Sprintf("(exit %d)", s.exitCode))
207214 }
208215
@@ -213,14 +220,23 @@ func (s *ShellItem) RawRender(width int) string {
213220 output := strings.TrimRight(s.output, "\n")
214221 lines := strings.Split(output, "\n")
215222
223+ // While streaming, show the tail of the output so the most recent
224+ // lines stay visible without forcing the user to expand.
216225 maxLines := shellMaxCollapsedLines
217226 if s.expandedContent {
218227 maxLines = len(lines)
219228 }
220229
221230 displayLines := lines
231+ truncatedCount := 0
222232 if len(lines) > maxLines {
223− displayLines = lines[:maxLines]
233+ if s.pending {
234+ // Show the most recent lines while still running.
235+ displayLines = lines[len(lines)-maxLines:]
236+ } else {
237+ displayLines = lines[:maxLines]
238+ }
239+ truncatedCount = len(lines) - maxLines
224240 }
225241
226242 // Compute max line width for scroll clamping.
@@ -234,6 +250,16 @@ func (s *ShellItem) RawRender(width int) string {
234250 s.maxLineWidth = max(0, maxW-cappedWidth)
235251
236252 var body strings.Builder
253+
254+ // When streaming, hidden lines are above the visible tail, so show
255+ // the "more lines" notice before the output.
256+ if truncatedCount > 0 && s.pending {
257+ body.WriteString(s.sty.Messages.ShellTruncation.Render(
258+ fmt.Sprintf("… %d earlier lines", truncatedCount),
259+ ))
260+ body.WriteString("\n")
261+ }
262+
237263 for _, ln := range displayLines {
238264 scrolled := ansi.GraphemeWidth.Cut(ln, s.xOffset, len(ln))
239265 truncated := ansi.Truncate(scrolled, cappedWidth, "…")
@@ -244,14 +270,20 @@ func (s *ShellItem) RawRender(width int) string {
244270 body.WriteString("\n")
245271 }
246272
247− if len(lines) > maxLines && !s.expandedContent {
273+ // When finished, hidden lines are below, so show the notice after.
274+ if truncatedCount > 0 && !s.pending && !s.expandedContent {
248275 body.WriteString(s.sty.Messages.ShellTruncation.Render(
249− fmt.Sprintf("… %d more lines", len(lines)-maxLines),
276+ fmt.Sprintf("… %d more lines", truncatedCount),
250277 ))
251− } else {
252− result := body.String()
253− return header + "\n" + strings.TrimRight(result, "\n")
278+ return header + "\n" + body.String()
279+ }
280+
281+ result := header + "\n" + strings.TrimRight(body.String(), "\n")
282+
283+ // While streaming, keep the spinner pinned below the latest output.
284+ if s.pending {
285+ result += "\n" + s.anim.Render()
254286 }
255287
256− return header + "\n" + strings.TrimRight(body.String(), "\n")
288+ return result
257289}