@@ -101,6 +101,12 @@ func (s *ShellItem) Complete(output string, exitCode int) {
104+// AppendOutput appends incremental output to a pending ShellItem.
105+func (s *ShellItem) AppendOutput(chunk string) {
104func (s *ShellItem) ID() string { return s.id }
110func (s *ShellItem) ID() string { return s.id }
105func (s *ShellItem) FilterValue() string { return s.command }
111func (s *ShellItem) FilterValue() string { return s.command }
106func (s *ShellItem) Finished() bool { return !s.pending }
112func (s *ShellItem) Finished() bool { return !s.pending }
@@ -199,10 +205,11 @@ func (s *ShellItem) RawRender(width int) string {
199 header := prompt + " " + highlighted
205 header := prompt + " " + highlighted
201 if s.pending {
207 if s.pending {
209+ // Nothing streamed yet: show the spinner under the header.
202− return header + "\n" + s.anim.Render()
210+ return header + "\n" + s.anim.Render()
205− if s.exitCode != 0 {
212+ } else if s.exitCode != 0 {
206 header += " " + s.sty.Messages.ShellExitCode.Render(fmt.Sprintf("(exit %d)", s.exitCode))
213 header += " " + s.sty.Messages.ShellExitCode.Render(fmt.Sprintf("(exit %d)", s.exitCode))
@@ -213,14 +220,23 @@ func (s *ShellItem) RawRender(width int) string {
213 output := strings.TrimRight(s.output, "\n")
220 output := strings.TrimRight(s.output, "\n")
214 lines := strings.Split(output, "\n")
221 lines := strings.Split(output, "\n")
223+ // While streaming, show the tail of the output so the most recent
224+ // lines stay visible without forcing the user to expand.
216 maxLines := shellMaxCollapsedLines
225 maxLines := shellMaxCollapsedLines
217 if s.expandedContent {
226 if s.expandedContent {
218 maxLines = len(lines)
227 maxLines = len(lines)
221 displayLines := lines
230 displayLines := lines
222 if len(lines) > maxLines {
232 if len(lines) > maxLines {
234+ // Show the most recent lines while still running.
235+ displayLines = lines[len(lines)-maxLines:]
223− displayLines = lines[:maxLines]
237+ displayLines = lines[:maxLines]
239+ truncatedCount = len(lines) - maxLines
226 // Compute max line width for scroll clamping.
242 // Compute max line width for scroll clamping.
@@ -234,6 +250,16 @@ func (s *ShellItem) RawRender(width int) string {
234 s.maxLineWidth = max(0, maxW-cappedWidth)
250 s.maxLineWidth = max(0, maxW-cappedWidth)
236 var body strings.Builder
252 var body strings.Builder
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),
260+ body.WriteString("\n")
237 for _, ln := range displayLines {
263 for _, ln := range displayLines {
238 scrolled := ansi.GraphemeWidth.Cut(ln, s.xOffset, len(ln))
264 scrolled := ansi.GraphemeWidth.Cut(ln, s.xOffset, len(ln))
239 truncated := ansi.Truncate(scrolled, cappedWidth, "…")
265 truncated := ansi.Truncate(scrolled, cappedWidth, "…")
@@ -244,14 +270,20 @@ func (s *ShellItem) RawRender(width int) string {
244 body.WriteString("\n")
270 body.WriteString("\n")
273+ // When finished, hidden lines are below, so show the notice after.
247− if len(lines) > maxLines && !s.expandedContent {
274+ if truncatedCount > 0 && !s.pending && !s.expandedContent {
248 body.WriteString(s.sty.Messages.ShellTruncation.Render(
275 body.WriteString(s.sty.Messages.ShellTruncation.Render(
249− fmt.Sprintf("… %d more lines", len(lines)-maxLines),
276+ fmt.Sprintf("… %d more lines", truncatedCount),
251− } else {
278+ return header + "\n" + body.String()
252− result := body.String()
279+ }
253− return header + "\n" + strings.TrimRight(result, "\n")
281+ result := header + "\n" + strings.TrimRight(body.String(), "\n")
283+ // While streaming, keep the spinner pinned below the latest output.
285+ result += "\n" + s.anim.Render()
256− return header + "\n" + strings.TrimRight(body.String(), "\n")
288+ return result