fix(chat): 修复会话刷新丢失最终答案(#3) + 反向分页/详情懒加载/会话URL持久化

- 后端消息列表默认按 id 升序取最旧一页,长会话刷新后最终答案落在页外而丢失
  (发新消息推进游标才重现)。改为:
  - db.ConvActivityPage:反向分页,取最新/更早一页 + hasMore
  - 消息接口按 since(增量)/before(更早页)/最新页 三种模式返回
- 前端聊天页:
  - 打开会话仅加载最新一页,滚动到顶部再反向加载更早(保持滚动位置不跳)
  - Transcript 的 UserRow/AnswerBlock 改为进入视口才拉 detail,避免打开会话即对
    整页每步发详情请求
  - 选中会话 id 持久化到 URL(?c=<id>),刷新回到当前会话
  - 修复新建会话后被"清理失效 id"effect 误清导致停留在新建页、输入框内容与灰色
    发送按钮卡死:移除该 effect + onStarted 乐观插入

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Autumn-27andClaude Opus 5 committed 2026-08-07 11:45:33 -04:00
1 parent 349b0d104d
commit 5000aadd8f
5 files changed
+207 -17

No files matched your search

+43
View File
@@ -174,6 +174,49 @@ FROM conversation_activities WHERE conversation_id=$1 AND id>$2 ORDER BY id LIMI
return out, cursor, rows.Err()
}
// ConvActivityPage returns one page for reverse (newest-first) pagination: up to
// `limit` steps ending before id `before` (exclusive; before<=0 = the latest
// page), returned in ASCENDING id order. hasMore reports whether still-older steps
// exist before the returned window, so the client can stop loading earlier history
// on scroll-up. Summary-only columns (detail is lazy-loaded via ConvActivityDetail).
func (d *DB) ConvActivityPage(convID, before int64, limit int) ([]Activity, bool, error) {
if limit <= 0 {
limit = 200
}
const cols = `id, COALESCE(worker,''), COALESCE(kind,''), COALESCE(tool,''), COALESCE(tool_use_id,''), is_error, COALESCE(summary,''), created_at, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens`
// fetch one extra row to detect whether older history remains before this window.
rows, err := d.Query(`SELECT `+cols+`
FROM conversation_activities
WHERE conversation_id=$1 AND ($2 <= 0 OR id < $2)
ORDER BY id DESC LIMIT $3`, convID, before, limit+1)
if err != nil {
return nil, false, err
}
defer rows.Close()
desc := []Activity{}
for rows.Next() {
var a Activity
if err := rows.Scan(&a.ID, &a.Worker, &a.Kind, &a.Tool, &a.ToolUseID, &a.IsError, &a.Summary, &a.CreatedAt,
&a.InputTokens, &a.OutputTokens, &a.CacheReadTokens, &a.CacheWriteTokens); err != nil {
return nil, false, err
}
desc = append(desc, a)
}
if err := rows.Err(); err != nil {
return nil, false, err
}
hasMore := len(desc) > limit
if hasMore {
desc = desc[:limit]
}
// reverse the newest-first window into ascending id order for display.
out := make([]Activity, len(desc))
for i, a := range desc {
out[len(desc)-1-i] = a
}
return out, hasMore, nil
}
// ConvActivityDetail lazily returns the full detail blob for one step.
func (d *DB) ConvActivityDetail(convID, id int64) (string, error) {
var s sql.NullString