Skip to content

Commit 41dca18

Browse files
committed
Fix inability to scroll content while AI is generating a response
Caused by b63fb68, we should not update the selection if it is not at the end
1 parent 666a3cf commit 41dca18

File tree

2 files changed

+116
-2
lines changed

2 files changed

+116
-2
lines changed

qgitc/aichatbot.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,15 @@ def appendResponse(self, response: AiResponse):
8787
cursor = self.textCursor()
8888
selectionStart = cursor.selectionStart()
8989
selectionEnd = cursor.selectionEnd()
90+
docLength = self.document().characterCount() - 1
9091
cursor.movePosition(QTextCursor.End)
9192

9293
if not response.is_delta or response.first_delta:
9394
self._insertRoleBlock(cursor, response.role)
9495
cursor.insertBlock()
9596
cursor.insertText(response.message)
9697

97-
if selectionStart != selectionEnd:
98+
if selectionStart != selectionEnd and selectionEnd == docLength:
9899
newCursor = self.textCursor()
99100
newCursor.setPosition(selectionStart)
100101
newCursor.setPosition(selectionEnd, QTextCursor.KeepAnchor)
@@ -104,14 +105,15 @@ def appendServiceUnavailable(self):
104105
cursor = self.textCursor()
105106
selectionStart = cursor.selectionStart()
106107
selectionEnd = cursor.selectionEnd()
108+
docLength = self.document().characterCount() - 1
107109
cursor.movePosition(QTextCursor.End)
108110

109111
self._insertRoleBlock(cursor, AiRole.System)
110112

111113
cursor.insertBlock()
112114
cursor.insertText(self.tr("Service Unavailable"))
113115

114-
if selectionStart != selectionEnd:
116+
if selectionStart != selectionEnd and selectionEnd == docLength:
115117
newCursor = self.textCursor()
116118
newCursor.setPosition(selectionStart)
117119
newCursor.setPosition(selectionEnd, QTextCursor.KeepAnchor)

tests/test_aichatbot.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# -*- coding: utf-8 -*-
2+
from PySide6.QtGui import QTextCursor
3+
from PySide6.QtTest import QTest
4+
5+
from qgitc.aichatbot import AiChatbot
6+
from qgitc.llm import AiResponse, AiRole
7+
from tests.base import TestBase
8+
9+
10+
class TestAiChatbot(TestBase):
11+
def setUp(self):
12+
super().setUp()
13+
self.chatbot = AiChatbot()
14+
self.chatbot.show()
15+
QTest.qWaitForWindowExposed(self.chatbot)
16+
17+
def tearDown(self):
18+
self.chatbot.close()
19+
super().tearDown()
20+
21+
def doCreateRepo(self):
22+
pass
23+
24+
def testAppendResponse_SelectionAtEnd_Preserved(self):
25+
"""Test that selection at document end IS preserved (prevents expansion)"""
26+
# Add initial content
27+
response1 = AiResponse(role=AiRole.Assistant, message="First message")
28+
response1.is_delta = False
29+
self.chatbot.appendResponse(response1)
30+
31+
# Select to the end of document
32+
cursor = self.chatbot.textCursor()
33+
cursor.movePosition(QTextCursor.Start)
34+
cursor.movePosition(QTextCursor.NextCharacter,
35+
QTextCursor.MoveAnchor, 5)
36+
cursor.movePosition(QTextCursor.End, QTextCursor.KeepAnchor)
37+
self.chatbot.setTextCursor(cursor)
38+
39+
selectionStart = cursor.selectionStart()
40+
selectionEnd = cursor.selectionEnd()
41+
selectedText = cursor.selectedText()
42+
43+
# Verify we ARE at the document end
44+
docLength = self.chatbot.document().characterCount() - 1
45+
self.assertEqual(selectionEnd, docLength)
46+
47+
# Append new content
48+
response2 = AiResponse(role=AiRole.User, message="Second message")
49+
response2.is_delta = False
50+
self.chatbot.appendResponse(response2)
51+
52+
# Verify selection is preserved (not expanded to include new text)
53+
newCursor = self.chatbot.textCursor()
54+
self.assertTrue(newCursor.hasSelection())
55+
self.assertEqual(selectionStart, newCursor.selectionStart())
56+
self.assertEqual(selectionEnd, newCursor.selectionEnd())
57+
self.assertEqual(selectedText, newCursor.selectedText())
58+
59+
def testAppendResponse_DeltaMessages(self):
60+
"""Test appending delta messages (streaming responses)"""
61+
# First delta
62+
response1 = AiResponse(role=AiRole.Assistant, message="Hello ")
63+
response1.is_delta = True
64+
response1.first_delta = True
65+
self.chatbot.appendResponse(response1)
66+
67+
cursor = self.chatbot.textCursor()
68+
cursor.movePosition(QTextCursor.End)
69+
endPos1 = cursor.position()
70+
71+
# Subsequent deltas
72+
response2 = AiResponse(role=AiRole.Assistant, message="World")
73+
response2.is_delta = True
74+
response2.first_delta = False
75+
self.chatbot.appendResponse(response2)
76+
77+
# Verify content is appended
78+
cursor = self.chatbot.textCursor()
79+
cursor.movePosition(QTextCursor.End)
80+
endPos2 = cursor.position()
81+
self.assertGreater(endPos2, endPos1)
82+
83+
# Verify text content
84+
text = self.chatbot.toPlainText()
85+
self.assertIn("Hello World", text)
86+
87+
def testAppendResponse_NoSelectionNoCursorChange(self):
88+
"""Test that when there's no selection and cursor is not at end, cursor stays in place"""
89+
# Add initial content
90+
response1 = AiResponse(role=AiRole.Assistant, message="First message")
91+
response1.is_delta = False
92+
self.chatbot.appendResponse(response1)
93+
94+
# Position cursor in the middle (no selection)
95+
cursor = self.chatbot.textCursor()
96+
cursor.movePosition(QTextCursor.Start)
97+
cursor.movePosition(QTextCursor.NextCharacter,
98+
QTextCursor.MoveAnchor, 5)
99+
self.chatbot.setTextCursor(cursor)
100+
cursorPos = cursor.position()
101+
self.assertFalse(cursor.hasSelection())
102+
103+
# Append new content
104+
response2 = AiResponse(role=AiRole.User, message="Second message")
105+
response2.is_delta = False
106+
self.chatbot.appendResponse(response2)
107+
108+
# Cursor should stay at the same position since there was no selection
109+
# and it wasn't at the end
110+
actualCursor = self.chatbot.textCursor()
111+
self.assertEqual(cursorPos, actualCursor.position())
112+
self.assertFalse(actualCursor.hasSelection())

0 commit comments

Comments
 (0)