fix(subagents): scroll bounded transcripts
- commit
- 765242b60b6245fe4665b7550cff7a81a6443acf
- author
- brice · · 2026-08-11 03:53 UTC
- parent
- 41cac35
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +58 -8
M packages/agent/modes/subagents_dialog.go +58 -8
| @@ -58,10 +58,11 @@ type subagentsDialog struct { | |||
| 58 | 58 | // its transcript view is opened. Nil preserves snapshot-only behavior. | |
| 59 | 59 | loadTranscript func(id string) error | |
| 60 | 60 | ||
| 61 | - | rows []subagents.AgentSnapshot | |
| 62 | - | cursor int | |
| 63 | - | maxRows int // total dialog rows, including its header and frame rule; zero is unbounded | |
| 64 | - | viewing bool // when true, show full transcript of the selected row | |
| 61 | + | rows []subagents.AgentSnapshot | |
| 62 | + | cursor int | |
| 63 | + | maxRows int // total dialog rows, including its header and frame rule; zero is unbounded | |
| 64 | + | viewing bool // when true, show full transcript of the selected row | |
| 65 | + | transcriptScroll int // display rows above the newest transcript viewport | |
| 65 | 66 | ||
| 66 | 67 | // transcriptSpin animates the busy hint shown above the inline | |
| 67 | 68 | // editor while the agent is mid-turn (status running, activity | |
| @@ -739,6 +740,34 @@ func (d *subagentsDialog) handleViewingKey(k tui.Key) (closed bool, msg, errMsg | |||
| 739 | 740 | return false, "", "" | |
| 740 | 741 | } | |
| 741 | 742 | ||
| 743 | + | // Transcript navigation owns the vertical navigation keys; horizontal | |
| 744 | + | // arrows remain available to the inline editor for caret movement. | |
| 745 | + | switch k.Kind { | |
| 746 | + | case tui.KeyUp: | |
| 747 | + | d.transcriptScroll++ | |
| 748 | + | return false, "", "" | |
| 749 | + | case tui.KeyDown: | |
| 750 | + | if d.transcriptScroll > 0 { | |
| 751 | + | d.transcriptScroll-- | |
| 752 | + | } | |
| 753 | + | return false, "", "" | |
| 754 | + | case tui.KeyPageUp: | |
| 755 | + | d.transcriptScroll += 10 | |
| 756 | + | return false, "", "" | |
| 757 | + | case tui.KeyPageDown: | |
| 758 | + | d.transcriptScroll -= 10 | |
| 759 | + | if d.transcriptScroll < 0 { | |
| 760 | + | d.transcriptScroll = 0 | |
| 761 | + | } | |
| 762 | + | return false, "", "" | |
| 763 | + | case tui.KeyHome: | |
| 764 | + | d.transcriptScroll = int(^uint(0) >> 1) | |
| 765 | + | return false, "", "" | |
| 766 | + | case tui.KeyEnd: | |
| 767 | + | d.transcriptScroll = 0 | |
| 768 | + | return false, "", "" | |
| 769 | + | } | |
| 770 | + | ||
| 742 | 771 | // Everything else — typed runes, Enter, Backspace, arrow keys | |
| 743 | 772 | // for caret movement, @-file picker — is editor input. If the | |
| 744 | 773 | // agent isn't running there's no live editor; surface a hint. | |
| @@ -1319,15 +1348,36 @@ func (d *subagentsDialog) renderTranscript(th tui.Theme, width int) []string { | |||
| 1319 | 1348 | // more above" cliff). | |
| 1320 | 1349 | body := renderSupervisorTranscriptBlocks(raw, th, width, d.compactMode) | |
| 1321 | 1350 | ||
| 1322 | - | out := header | |
| 1323 | - | out = append(out, body...) | |
| 1324 | - | ||
| 1325 | 1351 | // /btw-style inline editor pinned to the bottom of the transcript. | |
| 1326 | 1352 | // Rendered after the body so the editor sits flush above the | |
| 1327 | 1353 | // frame rule. Only shown for running agents (others have no live | |
| 1328 | 1354 | // inbox listener — we surface a hint instead). | |
| 1329 | - | out = d.appendTranscriptEditor(out, th, width, a) | |
| 1355 | + | footer := d.appendTranscriptEditor(nil, th, width, a) | |
| 1356 | + | if d.maxRows > 0 { | |
| 1357 | + | bodyRows := d.maxRows - len(header) - len(footer) - 1 | |
| 1358 | + | if bodyRows < 1 { | |
| 1359 | + | bodyRows = 1 | |
| 1360 | + | } | |
| 1361 | + | maxScroll := len(body) - bodyRows | |
| 1362 | + | if maxScroll < 0 { | |
| 1363 | + | maxScroll = 0 | |
| 1364 | + | } | |
| 1365 | + | if d.transcriptScroll > maxScroll { | |
| 1366 | + | d.transcriptScroll = maxScroll | |
| 1367 | + | } | |
| 1368 | + | start := len(body) - bodyRows - d.transcriptScroll | |
| 1369 | + | if start < 0 { | |
| 1370 | + | start = 0 | |
| 1371 | + | } | |
| 1372 | + | end := start + bodyRows | |
| 1373 | + | if end > len(body) { | |
| 1374 | + | end = len(body) | |
| 1375 | + | } | |
| 1376 | + | body = body[start:end] | |
| 1377 | + | } | |
| 1330 | 1378 | ||
| 1379 | + | out := append(header, body...) | |
| 1380 | + | out = append(out, footer...) | |
| 1331 | 1381 | out = append(out, frameRule(th, width)) | |
| 1332 | 1382 | return out | |
| 1333 | 1383 | } | |
| @@ -58,10 +58,11 @@ type subagentsDialog struct { | |||||
| 58 | // its transcript view is opened. Nil preserves snapshot-only behavior. | 58 | // its transcript view is opened. Nil preserves snapshot-only behavior. | ||
| 59 | loadTranscript func(id string) error | 59 | loadTranscript func(id string) error | ||
| 60 | 60 | ||||
| 61 | - | rows []subagents.AgentSnapshot | 61 | + | rows []subagents.AgentSnapshot |
| 62 | - | cursor int | 62 | + | cursor int |
| 63 | - | maxRows int // total dialog rows, including its header and frame rule; zero is unbounded | 63 | + | maxRows int // total dialog rows, including its header and frame rule; zero is unbounded |
| 64 | - | viewing bool // when true, show full transcript of the selected row | 64 | + | viewing bool // when true, show full transcript of the selected row |
| 65 | + | transcriptScroll int // display rows above the newest transcript viewport | |||
| 65 | 66 | ||||
| 66 | // transcriptSpin animates the busy hint shown above the inline | 67 | // transcriptSpin animates the busy hint shown above the inline | ||
| 67 | // editor while the agent is mid-turn (status running, activity | 68 | // editor while the agent is mid-turn (status running, activity | ||
| @@ -739,6 +740,34 @@ func (d *subagentsDialog) handleViewingKey(k tui.Key) (closed bool, msg, errMsg | |||||
| 739 | return false, "", "" | 740 | return false, "", "" | ||
| 740 | } | 741 | } | ||
| 741 | 742 | ||||
| 743 | + | // Transcript navigation owns the vertical navigation keys; horizontal | |||
| 744 | + | // arrows remain available to the inline editor for caret movement. | |||
| 745 | + | switch k.Kind { | |||
| 746 | + | case tui.KeyUp: | |||
| 747 | + | d.transcriptScroll++ | |||
| 748 | + | return false, "", "" | |||
| 749 | + | case tui.KeyDown: | |||
| 750 | + | if d.transcriptScroll > 0 { | |||
| 751 | + | d.transcriptScroll-- | |||
| 752 | + | } | |||
| 753 | + | return false, "", "" | |||
| 754 | + | case tui.KeyPageUp: | |||
| 755 | + | d.transcriptScroll += 10 | |||
| 756 | + | return false, "", "" | |||
| 757 | + | case tui.KeyPageDown: | |||
| 758 | + | d.transcriptScroll -= 10 | |||
| 759 | + | if d.transcriptScroll < 0 { | |||
| 760 | + | d.transcriptScroll = 0 | |||
| 761 | + | } | |||
| 762 | + | return false, "", "" | |||
| 763 | + | case tui.KeyHome: | |||
| 764 | + | d.transcriptScroll = int(^uint(0) >> 1) | |||
| 765 | + | return false, "", "" | |||
| 766 | + | case tui.KeyEnd: | |||
| 767 | + | d.transcriptScroll = 0 | |||
| 768 | + | return false, "", "" | |||
| 769 | + | } | |||
| 770 | + | ||||
| 742 | // Everything else — typed runes, Enter, Backspace, arrow keys | 771 | // Everything else — typed runes, Enter, Backspace, arrow keys | ||
| 743 | // for caret movement, @-file picker — is editor input. If the | 772 | // for caret movement, @-file picker — is editor input. If the | ||
| 744 | // agent isn't running there's no live editor; surface a hint. | 773 | // agent isn't running there's no live editor; surface a hint. | ||
| @@ -1319,15 +1348,36 @@ func (d *subagentsDialog) renderTranscript(th tui.Theme, width int) []string { | |||||
| 1319 | // more above" cliff). | 1348 | // more above" cliff). | ||
| 1320 | body := renderSupervisorTranscriptBlocks(raw, th, width, d.compactMode) | 1349 | body := renderSupervisorTranscriptBlocks(raw, th, width, d.compactMode) | ||
| 1321 | 1350 | ||||
| 1322 | - | out := header | |||
| 1323 | - | out = append(out, body...) | |||
| 1324 | - | ||||
| 1325 | // /btw-style inline editor pinned to the bottom of the transcript. | 1351 | // /btw-style inline editor pinned to the bottom of the transcript. | ||
| 1326 | // Rendered after the body so the editor sits flush above the | 1352 | // Rendered after the body so the editor sits flush above the | ||
| 1327 | // frame rule. Only shown for running agents (others have no live | 1353 | // frame rule. Only shown for running agents (others have no live | ||
| 1328 | // inbox listener — we surface a hint instead). | 1354 | // inbox listener — we surface a hint instead). | ||
| 1329 | - | out = d.appendTranscriptEditor(out, th, width, a) | 1355 | + | footer := d.appendTranscriptEditor(nil, th, width, a) |
| 1356 | + | if d.maxRows > 0 { | |||
| 1357 | + | bodyRows := d.maxRows - len(header) - len(footer) - 1 | |||
| 1358 | + | if bodyRows < 1 { | |||
| 1359 | + | bodyRows = 1 | |||
| 1360 | + | } | |||
| 1361 | + | maxScroll := len(body) - bodyRows | |||
| 1362 | + | if maxScroll < 0 { | |||
| 1363 | + | maxScroll = 0 | |||
| 1364 | + | } | |||
| 1365 | + | if d.transcriptScroll > maxScroll { | |||
| 1366 | + | d.transcriptScroll = maxScroll | |||
| 1367 | + | } | |||
| 1368 | + | start := len(body) - bodyRows - d.transcriptScroll | |||
| 1369 | + | if start < 0 { | |||
| 1370 | + | start = 0 | |||
| 1371 | + | } | |||
| 1372 | + | end := start + bodyRows | |||
| 1373 | + | if end > len(body) { | |||
| 1374 | + | end = len(body) | |||
| 1375 | + | } | |||
| 1376 | + | body = body[start:end] | |||
| 1377 | + | } | |||
| 1330 | 1378 | ||||
| 1379 | + | out := append(header, body...) | |||
| 1380 | + | out = append(out, footer...) | |||
| 1331 | out = append(out, frameRule(th, width)) | 1381 | out = append(out, frameRule(th, width)) | ||
| 1332 | return out | 1382 | return out | ||
| 1333 | } | 1383 | } | ||