detached 765242b

fix(subagents): scroll bounded transcripts

commit
765242b60b6245fe4665b7550cff7a81a6443acf
author
brice · · 2026-08-11 03:53 UTC
parent
41cac35
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
5858 // its transcript view is opened. Nil preserves snapshot-only behavior.
5959 loadTranscript func(id string) error
6060
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
6566
6667 // transcriptSpin animates the busy hint shown above the inline
6768 // 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
739740 return false, "", ""
740741 }
741742
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+
742771 // Everything else — typed runes, Enter, Backspace, arrow keys
743772 // for caret movement, @-file picker — is editor input. If the
744773 // 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 {
13191348 // more above" cliff).
13201349 body := renderSupervisorTranscriptBlocks(raw, th, width, d.compactMode)
13211350
1322- out := header
1323- out = append(out, body...)
1324-
13251351 // /btw-style inline editor pinned to the bottom of the transcript.
13261352 // Rendered after the body so the editor sits flush above the
13271353 // frame rule. Only shown for running agents (others have no live
13281354 // 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+ }
13301378
1379+ out := append(header, body...)
1380+ out = append(out, footer...)
13311381 out = append(out, frameRule(th, width))
13321382 return out
13331383 }
@@ -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) error59 loadTranscript func(id string) error
60 60
61- rows []subagents.AgentSnapshot61+ rows []subagents.AgentSnapshot
62- cursor int62+ cursor int
63- maxRows int // total dialog rows, including its header and frame rule; zero is unbounded63+ 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 row64+ 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 inline67 // transcriptSpin animates the busy hint shown above the inline
67 // editor while the agent is mid-turn (status running, activity68 // 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 keys771 // Everything else — typed runes, Enter, Backspace, arrow keys
743 // for caret movement, @-file picker — is editor input. If the772 // 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 the1352 // Rendered after the body so the editor sits flush above the
1327 // frame rule. Only shown for running agents (others have no live1353 // 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 out1382 return out
1333 }1383 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close