Skip to content

Commit e6e3945

Browse files
xgopilotcreatang
andcommitted
fix(tui): address transcript selection high/medium risks
- clear stale completed selection when transcript content changes - ignore blank viewport rows when mapping mouse selection - preserve ansi runs outside selected range during highlight - skip redundant redraw on unchanged drag position Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: creatang <165447160+creatang@users.noreply.github.com>
1 parent 998a35e commit e6e3945

4 files changed

Lines changed: 103 additions & 6 deletions

File tree

internal/tui/core/app/copy_code.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,9 @@ func (a App) selectionPositionAtMouse(msg tea.MouseMsg) (line int, col int, ok b
262262
currentLine := a.transcript.YOffset + (msg.Y - y)
263263
currentCol := msg.X - x
264264
lines := a.selectionLines()
265+
if len(lines) == 0 || currentLine < 0 || currentLine >= len(lines) {
266+
return 0, 0, false
267+
}
265268
return a.normalizeSelectionPosition(lines, currentLine, currentCol)
266269
}
267270

@@ -309,6 +312,9 @@ func (a *App) updateTextSelection(msg tea.MouseMsg) bool {
309312
if !ok {
310313
return false
311314
}
315+
if a.textSelection.endLine == line && a.textSelection.endCol == col {
316+
return true
317+
}
312318
a.textSelection.endLine = line
313319
a.textSelection.endCol = col
314320
a.refreshTranscriptHighlight()

internal/tui/core/app/copy_code_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,89 @@ func TestSelectionPositionAndDragGuardBranches(t *testing.T) {
213213
}
214214
}
215215

216+
func TestSelectionPositionAtMouseRejectsBlankViewportRows(t *testing.T) {
217+
app, _ := newTestApp(t)
218+
app.width = 100
219+
app.height = 24
220+
app.applyComponentLayout(true)
221+
app.setTranscriptContent("only-one-line")
222+
223+
x, y, _, h := app.transcriptBounds()
224+
if h < 2 {
225+
t.Fatalf("expected transcript viewport with spare rows, got height=%d", h)
226+
}
227+
228+
if _, _, ok := app.selectionPositionAtMouse(tea.MouseMsg{X: x + 1, Y: y + h - 1}); ok {
229+
t.Fatalf("expected blank viewport row to be ignored")
230+
}
231+
}
232+
233+
func TestSetTranscriptContentClearsSelectionAfterContentChange(t *testing.T) {
234+
app, _ := newTestApp(t)
235+
app.width = 100
236+
app.height = 24
237+
app.applyComponentLayout(true)
238+
app.setTranscriptContent("line-one")
239+
app.textSelection.active = true
240+
app.textSelection.startLine = 0
241+
app.textSelection.startCol = 0
242+
app.textSelection.endLine = 0
243+
app.textSelection.endCol = 4
244+
app.refreshTranscriptHighlight()
245+
246+
app.setTranscriptContent("line-two")
247+
if app.textSelection.active || app.textSelection.dragging {
248+
t.Fatalf("expected selection to be cleared after transcript content changes")
249+
}
250+
if app.hasTextSelection() {
251+
t.Fatalf("expected no valid selection range after transcript content changes")
252+
}
253+
}
254+
255+
func TestUpdateTextSelectionSkipsUnchangedPosition(t *testing.T) {
256+
app, _ := newTestApp(t)
257+
app.width = 100
258+
app.height = 24
259+
app.applyComponentLayout(true)
260+
app.setTranscriptContent("alpha\nbeta")
261+
262+
x, y, _, _ := app.transcriptBounds()
263+
if !app.beginTextSelection(tea.MouseMsg{X: x + 1, Y: y + 1}) {
264+
t.Fatalf("expected beginTextSelection to succeed")
265+
}
266+
if !app.updateTextSelection(tea.MouseMsg{X: x + 2, Y: y + 1}) {
267+
t.Fatalf("expected first updateTextSelection to succeed")
268+
}
269+
270+
app.transcript.SetContent("sentinel-marker")
271+
if !app.updateTextSelection(tea.MouseMsg{X: x + 2, Y: y + 1}) {
272+
t.Fatalf("expected unchanged motion to be handled")
273+
}
274+
if !strings.Contains(app.transcript.View(), "sentinel-marker") {
275+
t.Fatalf("expected unchanged motion to skip redraw")
276+
}
277+
}
278+
279+
func TestHighlightTranscriptContentPreservesANSIOutsideSelection(t *testing.T) {
280+
app, _ := newTestApp(t)
281+
app.width = 100
282+
app.height = 24
283+
app.applyComponentLayout(true)
284+
app.textSelection.active = true
285+
app.textSelection.startLine = 0
286+
app.textSelection.startCol = 6
287+
app.textSelection.endLine = 0
288+
app.textSelection.endCol = 11
289+
290+
highlighted := app.highlightTranscriptContent("\x1b[31mhello world\x1b[0m")
291+
if !strings.Contains(highlighted, "\x1b[31m") {
292+
t.Fatalf("expected highlighted content to preserve existing ANSI style runs")
293+
}
294+
if plain := copyCodeANSIPattern.ReplaceAllString(highlighted, ""); plain != "hello world" {
295+
t.Fatalf("expected highlighted content to preserve visible text, got %q", plain)
296+
}
297+
}
298+
216299
func TestCopySelectionToClipboardNoSelectionNoop(t *testing.T) {
217300
app, _ := newTestApp(t)
218301
app.setTranscriptContent("hello")

internal/tui/core/app/update.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2344,6 +2344,15 @@ func (a *App) rebuildTranscript() {
23442344

23452345
func (a *App) setTranscriptContent(content string) {
23462346
normalized := normalizeTranscriptForDisplay(content)
2347+
contentChanged := a.transcriptContent != normalized
2348+
if contentChanged && a.textSelection.active && !a.textSelection.dragging {
2349+
a.textSelection.active = false
2350+
a.textSelection.dragging = false
2351+
a.textSelection.startLine = 0
2352+
a.textSelection.startCol = 0
2353+
a.textSelection.endLine = 0
2354+
a.textSelection.endCol = 0
2355+
}
23472356
a.transcriptContent = normalized
23482357
if a.hasTextSelection() {
23492358
a.transcript.SetContent(a.highlightTranscriptContent(normalized))
@@ -2364,8 +2373,7 @@ func (a *App) highlightTranscriptContent(content string) string {
23642373
Foreground(lipgloss.Color(selectionFg))
23652374

23662375
for i := startLine; i <= endLine && i < len(lines); i++ {
2367-
plain := copyCodeANSIPattern.ReplaceAllString(lines[i], "")
2368-
lineWidth := lipgloss.Width(plain)
2376+
lineWidth := ansi.StringWidth(lines[i])
23692377
selStart := 0
23702378
selEnd := lineWidth
23712379
if i == startLine {
@@ -2379,9 +2387,9 @@ func (a *App) highlightTranscriptContent(content string) string {
23792387
if selEnd <= selStart {
23802388
continue
23812389
}
2382-
prefix := ansi.Cut(plain, 0, selStart)
2383-
selected := ansi.Cut(plain, selStart, selEnd)
2384-
suffix := ansi.Cut(plain, selEnd, lineWidth)
2390+
prefix := ansi.Cut(lines[i], 0, selStart)
2391+
selected := ansi.Cut(lines[i], selStart, selEnd)
2392+
suffix := ansi.Cut(lines[i], selEnd, lineWidth)
23852393
lines[i] = prefix + highlightStyle.Render(selected) + suffix
23862394
}
23872395
return strings.Join(lines, "\n")

internal/tui/core/app/view_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,12 @@ func TestRenderWaterfallThinkingState(t *testing.T) {
128128
func TestRenderWaterfallSelectionHint(t *testing.T) {
129129
app, _ := newTestApp(t)
130130
app.state.ActivePicker = pickerNone
131+
app.setTranscriptContent("hello")
131132
app.textSelection.active = true
132133
app.textSelection.startLine = 0
133134
app.textSelection.startCol = 0
134135
app.textSelection.endLine = 0
135136
app.textSelection.endCol = 1
136-
app.setTranscriptContent("hello")
137137

138138
view := app.renderWaterfall(80, 24)
139139
if !strings.Contains(view, "已选择内容,右键复制") {

0 commit comments

Comments
 (0)