Skip to content

feat(chat): add scroll speed limit for streaming output and top positioning for non-streaming output (#1116) - #1120

Open
3316891527 wants to merge 2 commits into
AAswordman:devfrom
3316891527:feat/chat-scroll-speed-limit
Open

feat(chat): add scroll speed limit for streaming output and top positioning for non-streaming output (#1116)#1120
3316891527 wants to merge 2 commits into
AAswordman:devfrom
3316891527:feat/chat-scroll-speed-limit

Conversation

@3316891527

@3316891527 3316891527 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

变更说明 / Description

背景与动机 / Context and motivation

针对 Issue #1116 反映的两个对话阅读体验问题:

  1. 流式输出时页面滚动过快:高吞吐量模型高速吐字时,视线被剧烈拖拽下移,来不及阅读;
  2. 非流式输出时直接跳到底部:单次完整返回长回答时,页面直接掉入底部,需要手动向上翻找开头。

改动范围 / Changes

1. 流式输出滚动速度上限(21 档可调滑块)

将原先的二值开关(硬编码 800 dp/s)升级为连续可调滑块,共 21 档:

档位 速度
第 1 档 50 dp/s
第 2–20 档 100 ~ 1000 dp/s(步进 50)
第 21 档(最后) 不设上限,跟随模型吞吐量
  • 默认值:600 dp/s
  • 这是上限而非固定速度:模型输出较慢时完全不介入,仅在超速时拉长动画时长削峰,慢模型体验不受影响
  • 设置入口:【显示与行为 → 消息显示设置 → 流式输出滚动速度上限】

2. 非流式输出定位到消息开头

  • AI 单次完整回复时,页面自动平滑定位至该条消息顶部,默认开启
  • 设置入口:【显示与行为 → 消息显示设置 → 非流式输出定位到消息开头】

3. 核心文件改动

  • DisplayPreferencesManager.kt:原布尔键替换为 intstream_scroll_max_speed_dp,新增 21 档选项列表与"不限速"标记常量
  • ChatScrollSpeedLimit.kt:参数从 enableLimit: Boolean 改为 maxSpeedDpPerSecond: Int,传入不限速常量时走直通路径
  • ChatArea.kt / AIChatScreen.kt:读取新的速度 Flow,流式时传速度上限值,非流式时传不限速
  • GlobalDisplaySettingsScreen.kt:开关替换为 Slider,档位列表驱动,实时显示当前 dp/s 值(最后档显示"不设上限")
  • 8 种语言 strings.xml:旧开关文案替换为 title / description / value / unlimited 四条本地化字符串

4. 明确不包含

  • 未改动网络请求、LLM 生成管道及消息持久化结构
  • 未修改任何数据库 schema

兼容性与风险 / Compatibility and risks

  • 旧版保存的布尔值废弃后,存量用户首次打开将回落到默认值 600 dp/s,不会报错
  • 风险低,仅约束动画插值时长,不涉及底层消息布局与内容状态

关联 Issue / Related issue

Resolves #1116

验证方式 / Verification

  • 单元测试 ChatScrollSpeedLimitTest:6 组断言,覆盖比例计算、上下界 clamp、档位列表结构(21 档、首尾值、默认值可选)
  • CI android-build.yml assembleDebug:run 34031171863,结论 success
  • 实机测试(@bylt-max):非流式定位已符合预期;流式限速效果明显改善,800 dp/s 偏快,已据此将默认值调为 600 dp/s 并改为可调

检查清单 / Checklist

  • 我已记录可复现验证和未运行项原因
  • 日常开发 PR 的目标分支为 dev
  • 最终 diff 无无关、临时、生成、二进制或敏感内容
  • 已提供对应的回归、UI、文档/字符串或兼容性证据

@CATMIAOZHI CATMIAOZHI left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCK:发现 3 项 P2,具体触发条件和修正建议见行内评论。

审计提交:df3b01da86bee4ceaf2a76c25fc6c55e9d5e271a。已完成独立工作树全 diff 与调用链审查、独立 agent 复核及 diff --check;审计前已请求 CATMIAOZHI review。

验证范围:当前上游仓库该 HEAD 的 check-runs 为 0,审计前无 review 线程。现有 PR workflow 仅匹配 development,而本 PR 目标为 dev;构建/测试 push workflow 匹配 main。已核对仓库 Actions Variables(GITEE_OWNER、GITEE_REPO)及工作流任务。已核实作者 fork 的 Android Build 对应本次 HEAD 且成功;该运行的步骤只执行构建,没有 JVM 单测执行步骤,因此不将其视为单测通过证明。遵守目标仓库 AGENTS.md,本次未运行本地编译、测试、Lint 或真机验证;未修改源码。

}
val deltaDp = deltaPx / densityDpiRatio
val calculatedMs = ((deltaDp / maxSpeedDpPerSecond) * 1000f).toLong()
return calculatedMs.coerceIn(minDurationMs, maxDurationMs).toInt()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 不要用 1 秒时长上限突破用户设置的滚动速度

当流式输出积压 500 dp、用户选择 50 dp/s 时,计算需要 10 秒,但这里截成 1000 ms,后面的 LinearEasing 动画仍滚完全部距离,实际达到 500 dp/s。ChatArea 和 AIChatScreen 的追赶滚动都会走此函数,因此大块输出时恰好无法保持设置的上限。请保留满足限速所需的时长,或限制每段滚动位移。

val isTargetAi = targetMessage?.sender == "ai"
val isNonStreamingAi = isTargetAi &&
targetMessage?.contentStream == null &&
targetMessage?.timestamp !in streamedAiMessageTimestamps

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 根据实际输出模式识别非流式回复

普通聊天关闭 Waifu、开启“禁用流式输出”时,AIMessageManager 虽将 stream=false 传入请求,仍无条件调用 shareRevisable;MessageProcessingDelegate 随后把带 contentStream 的占位 AI 消息加入界面。下面的 effect 因此会将 timestamp 记入 streamedAiMessageTimestamps,完成后即使清掉 contentStream,该记录仍在,这里也不能进入顶部定位分支。请传递实际请求的流式模式,避免把 UI 使用共享流当成网络流式输出的证据。

if (isNonStreamingAi && enableNonStreamingScrollToTop) {
val targetOffset =
targetAnchor.absoluteTopPx.roundToInt().coerceIn(0, scrollState.maxValue)
scrollState.animateScrollTo(targetOffset)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 保留用户显式“滚动到底部”的导航行为

默认启用该选项后,重开以长 AI 回答结尾的历史并向上拖动,再点击底部按钮:ChatScrollNavigator 先启动到底动画并设置 autoScroll=true;后者触发 ChatArea 上方 effect,将最新消息设为 pending。历史消息没有 contentStream,且本次进入的 streamed 集合为空,于是这里又启动到消息开头的动画,打断或覆盖用户的到底请求。请把新非流式回复的自动顶部定位与用户显式到底导航区分开。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants