feat(tui-go): transcript message-jump + OSC52 copy
ctrl+↑/↓ jumps between your sent (user) messages in the transcript: ctrl+↑ from the bottom selects the most recent, steps to older ones (clamping); ctrl+↓ steps back and drops to tail-follow past the last. The selected message is highlighted and scrolled into view (routerRows tracks per-message row offsets and windows to the selection). `y` yanks the selected message — or the latest turn when none is selected — to the system clipboard over OSC52 (go-osc52, tmux-wrapped under $TMUX), which is SSH/Termius- safe unlike a mouse drag. A brief "✓ copied" footer note animates out (and self-stops the tick). esc drops the selection; switching sessions clears it. Tests cover the user-message jump/clamp/tail and the copy-text selection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -244,9 +244,12 @@ func (m Model) renderFooter() string {
|
||||
}
|
||||
default: // StateInSession
|
||||
if nrw {
|
||||
hints = []string{hint("i", "msg"), hint("e", "events"), hint("S", "stats"), hint("l", "back"), hint("p", "cmds"), hint("q", "quit")}
|
||||
hints = []string{hint("i", "msg"), hint("^↑↓", "msgs"), hint("y", "copy"), hint("e", "events"), hint("l", "back"), hint("p", "cmds"), hint("q", "quit")}
|
||||
} else {
|
||||
hints = []string{hint("i", "message"), hint("e", "events"), hint("t", "tools"), hint("S", "stats"), hint("m", "model"), hint("s", "mode"), hint("l", "back"), hint("p", "cmds"), hint("q", "quit")}
|
||||
hints = []string{hint("i", "message"), hint("^↑↓", "msgs"), hint("y", "copy"), hint("e", "events"), hint("t", "tools"), hint("S", "stats"), hint("m", "model"), hint("l", "back"), hint("p", "cmds"), hint("q", "quit")}
|
||||
}
|
||||
if m.copiedFlash != 0 && m.frame-m.copiedFlash < copiedFlashFrames {
|
||||
hints = append(hints, lipgloss.NewStyle().Foreground(t.P.OK).Background(bg).Bold(true).Render("✓ copied"))
|
||||
}
|
||||
if s := m.session(m.selectedID); s != nil && s.Pending != nil {
|
||||
hints = append(hints, hint("a", "approval (pending)"))
|
||||
@@ -488,9 +491,18 @@ func (m Model) routerRows(w, h int) []string {
|
||||
return []string{t.span("awaiting router response…", t.P.Faint)}
|
||||
}
|
||||
var rows []string
|
||||
for _, e := range msgs {
|
||||
msgStart := make([]int, len(msgs)) // first row index of each message, for scroll-to-selection
|
||||
for mi, e := range msgs {
|
||||
msgStart[mi] = len(rows)
|
||||
switch e.Role {
|
||||
case "user":
|
||||
if mi == m.transcriptSel {
|
||||
// Selected message: an inverted tag + a highlighted background bar so it's
|
||||
// unmistakable which one ctrl+↑/↓ landed on (and what `y` will copy).
|
||||
tag := lipgloss.NewStyle().Foreground(t.P.BgDeep).Background(t.P.Accent).Bold(true).Render(" › ")
|
||||
rows = append(rows, tag+" "+lipgloss.NewStyle().Foreground(t.P.FgStrong).Background(t.P.Sel).Render(e.Content))
|
||||
break
|
||||
}
|
||||
tag := lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Bold(true).Render("›")
|
||||
rows = append(rows, tag+" "+t.span(e.Content, t.P.FgStrong))
|
||||
case "router":
|
||||
@@ -525,7 +537,18 @@ func (m Model) routerRows(w, h int) []string {
|
||||
rows = append(rows, t.span(e.Content, t.P.Dim))
|
||||
}
|
||||
}
|
||||
// keep last h rows
|
||||
// With a selection, scroll so the selected message is visible (top-aligned, clamped
|
||||
// to the end); otherwise tail-follow the newest output.
|
||||
if m.transcriptSel >= 0 && m.transcriptSel < len(msgStart) && len(rows) > h {
|
||||
start := msgStart[m.transcriptSel]
|
||||
if start > len(rows)-h {
|
||||
start = len(rows) - h
|
||||
}
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
return rows[start : start+h]
|
||||
}
|
||||
if len(rows) > h {
|
||||
rows = rows[len(rows)-h:]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user