Skip to content

Commit 55cc7fb

Browse files
fix(acp): scroll to the first question when a form appears
A new ask_user form scrolled the transcript to the bottom, so long forms opened on their last question. The view now scrolls the form's top into view and stops following the stream until the next send, so text the agent writes after the form does not push it away. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent aeedb28 commit 55cc7fb

2 files changed

Lines changed: 33 additions & 1 deletion

File tree

‎src/widgets/AcpSessionView.cpp‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ bool askUserCallDropped(const QString &status)
9595
// bottom". Used both for the user's at-bottom recognition (the flag flips on
9696
// when they reach this band) and for the programmatic scroll target.
9797
constexpr int kAtBottomEpsilonPx = 8;
98+
// Space kept above a form scrolled into view, and a second pass once the
99+
// form's own layout has settled.
100+
constexpr int kFormScrollMarginPx = 8;
101+
constexpr int kFormScrollSettleMs = 60;
98102
constexpr int kMaxSkillCompletions = 256;
99103

100104
bool isEffortConfigOption(const AcpProtocol::AcpConfigOption &opt)
@@ -2206,9 +2210,9 @@ void AcpSessionView::presentForm(const AskUserForm::Spec &spec, FormSource sourc
22062210
auto *form = new AskUserFormWidget(spec, initial, m_transcriptHost);
22072211
form->setChatFont(chatFont());
22082212
insertTimelineWidget(form);
2209-
scrollToBottomDeferred();
22102213

22112214
if (isUnattended()) {
2215+
scrollToBottomDeferred();
22122216
const bool goal = m_goalRunning;
22132217
form->setState(AskUserFormWidget::State::Cancelled,
22142218
goal ? tr("Cancelled — goal agent active") : tr("Cancelled — unattended session"));
@@ -2234,6 +2238,32 @@ void AcpSessionView::presentForm(const AskUserForm::Spec &spec, FormSource sourc
22342238
// Take focus only from an empty composer the user is sitting in.
22352239
if (m_input && m_input->hasFocus() && m_input->toPlainText().trimmed().isEmpty())
22362240
form->focusFirstQuestion();
2241+
scrollToFormTop(form);
2242+
}
2243+
2244+
void AcpSessionView::scrollToFormTop(QWidget *form)
2245+
{
2246+
// Show question 1, not the bottom: stop following the stream (text the
2247+
// agent writes after the form would push it away) until the next send.
2248+
m_stickToBottom = false;
2249+
updateJumpButtonVisibility();
2250+
QPointer<AcpSessionView> guard(this);
2251+
QPointer<QWidget> target(form);
2252+
auto apply = [guard, target]() {
2253+
if (!guard || !target || !guard->m_scroll || !guard->m_transcriptHost)
2254+
return;
2255+
auto *vb = guard->m_scroll->verticalScrollBar();
2256+
if (!vb)
2257+
return;
2258+
const int top = target->mapTo(guard->m_transcriptHost, QPoint(0, 0)).y() - kFormScrollMarginPx;
2259+
guard->m_programmaticScroll = true;
2260+
vb->setValue(qBound(vb->minimum(), top, vb->maximum()));
2261+
guard->m_programmaticScroll = false;
2262+
guard->updateJumpButtonVisibility();
2263+
};
2264+
// Once after the insert is laid out, once more after the form has sized.
2265+
QTimer::singleShot(0, this, apply);
2266+
QTimer::singleShot(kFormScrollSettleMs, this, apply);
22372267
}
22382268

22392269
void AcpSessionView::settlePendingForm(AskUserFormWidget::State state, const QString &detail,

‎src/widgets/AcpSessionView.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,8 @@ private slots:
267267
bool fromGoalAgent = false);
268268
bool isUnattended() const;
269269
void restoreExpiredForm();
270+
// Scroll so the form's first question is at the top; stops following the stream.
271+
void scrollToFormTop(QWidget *form);
270272
void applyMessageMarker(AcpMessageWidget *w, int messageIndex);
271273
bool inputKeyEventIsSubmit(QKeyEvent *ke) const;
272274
// Heartbeat indicator at the tail of the transcript: shows time since the

0 commit comments

Comments
 (0)