优化项目对话、刷新续流、实时滚动与工具状态恢复 (#245)

* feat(chat): add project-based conversation sidebar

* feat(chat): refine Codex-style conversation UI

* feat(chat): add Codex-style conversation workflow

* feat(ui): 优化对话框与项目侧边栏交互

* fix(chat): 修复暗色输入框圆角填色

* fix(chat): 恢复输入区分层错位布局

* fix(hitl): isolate reviewer state per conversation

* feat(hitl): 增加双入口审批与倒计时进度

* fix(hitl): 汇总项目审批并隔离对话状态

* fix(ui): 修复审批状态与无项目新任务

* fix(ui): 优化审批状态与对话切换性能

* fix(ui): 修复中断任务审批仍计时

* fix(ui): 修复多对话并发切换卡顿

* fix(hitl): 主动同步审批并关闭中断状态

* fix(ui): 固定项目审批汇总为绿色

* feat(ui): 同步系统模型与推理强度

* feat(ui): 完善项目侧栏预览与新建入口

* fix(ui): 防止无项目文件夹误展开

* fix(ui): 防止长历史对话滚动误触审批

* fix(ui): 修复对话操作并补充项目置顶

* fix(ui): 移除对话分组并调整项目置顶排序

* feat(ui): 优化迭代导航与审批交互

* fix(hitl): 将 write_file 加入内置免审批工具

* fix(chat): 支持回车发送与 Shift 回车换行

* fix(chat): 优化对话刷新与 Codex 风格交互

* fix(ui): 显示对话具体更新时间

* fix(ui): 优化对话刷新与项目加载

* fix(ui): 修复 Agent 审查文字裁切

* fix(chat): 修复刷新续流与多标签页同步

* fix(chat): 修复滚动跟随与中断任务终态

* fix(ui): 修复流式滚动跳动与工具状态恢复

* fix(ui): 修复刷新后流式输出停止粘底
This commit is contained in:
RuoJi6
2026-08-13 21:28:37 +08:00
committed by GitHub
parent b170f2c4b1
commit eb6bab574f
31 changed files with 10763 additions and 886 deletions
+85
View File
@@ -1353,6 +1353,39 @@ func (db *DB) AddProcessDetailWithID(messageID, conversationID, eventType, messa
return id, nil
}
// UpdateProcessDetailContent 更新流式聚合详情的正文与元数据。使用固定记录 ID,
// 避免每个 token 新增一行,同时让页面刷新能读取到尚未结束的规划输出。
func (db *DB) UpdateProcessDetailContent(id, message string, data interface{}) error {
var dataJSON string
if data != nil {
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("序列化过程详情数据失败: %w", err)
}
dataJSON = string(jsonData)
}
result, err := db.Exec(
"UPDATE process_details SET message = ?, data = ? WHERE id = ?",
message, dataJSON, strings.TrimSpace(id),
)
if err != nil {
return fmt.Errorf("更新过程详情失败: %w", err)
}
if affected, affectedErr := result.RowsAffected(); affectedErr == nil && affected == 0 {
return fmt.Errorf("过程详情不存在: %s", id)
}
return nil
}
// DeleteProcessDetail 删除被判定为工具结果回显的临时规划记录。
func (db *DB) DeleteProcessDetail(id string) error {
_, err := db.Exec("DELETE FROM process_details WHERE id = ?", strings.TrimSpace(id))
if err != nil {
return fmt.Errorf("删除过程详情失败: %w", err)
}
return nil
}
// GetProcessDetails 获取消息的过程详情
func (db *DB) GetProcessDetails(messageID string) ([]ProcessDetail, error) {
rows, err := db.Query(
@@ -1420,6 +1453,10 @@ type ProcessDetailsSummary struct {
ToolCount int `json:"toolCount"`
ToolExecutions []ProcessDetailsToolExecution `json:"toolExecutions,omitempty"`
MCPExecutionIDs []string `json:"mcpExecutionIds,omitempty"`
StartedAt *time.Time `json:"startedAt,omitempty"`
CompletedAt *time.Time `json:"completedAt,omitempty"`
DurationMs int64 `json:"durationMs"`
Status string `json:"status,omitempty"`
}
type ProcessDetailsToolExecution struct {
@@ -1442,6 +1479,54 @@ func (db *DB) GetProcessDetailsSummary(messageID string) (*ProcessDetailsSummary
}
summary := &ProcessDetailsSummary{Total: total}
var messageCreatedAt, messageUpdatedAt sql.NullString
var messageContent string
if err := db.QueryRow(
"SELECT created_at, updated_at, content FROM messages WHERE id = ?",
messageID,
).Scan(&messageCreatedAt, &messageUpdatedAt, &messageContent); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("查询过程详情耗时失败: %w", err)
}
if messageCreatedAt.Valid {
if startedAt := parseDBTime(messageCreatedAt.String); !startedAt.IsZero() {
summary.StartedAt = &startedAt
}
}
var terminalEvent, terminalCreatedAt string
terminalErr := db.QueryRow(`
SELECT event_type, created_at
FROM process_details
WHERE message_id = ? AND event_type IN ('cancelled', 'timeout', 'error')
ORDER BY created_at DESC, rowid DESC
LIMIT 1`, messageID).Scan(&terminalEvent, &terminalCreatedAt)
if terminalErr != nil && !errors.Is(terminalErr, sql.ErrNoRows) {
return nil, fmt.Errorf("查询过程详情终态失败: %w", terminalErr)
}
if terminalEvent != "" {
switch terminalEvent {
case "cancelled":
summary.Status = "cancelled"
case "timeout":
summary.Status = "timeout"
default:
summary.Status = "failed"
}
if completedAt := parseDBTime(terminalCreatedAt); !completedAt.IsZero() {
summary.CompletedAt = &completedAt
}
} else if strings.TrimSpace(messageContent) == "处理中..." || strings.TrimSpace(messageContent) == "Processing..." {
summary.Status = "running"
} else {
summary.Status = "completed"
if messageUpdatedAt.Valid {
if completedAt := parseDBTime(messageUpdatedAt.String); !completedAt.IsZero() {
summary.CompletedAt = &completedAt
}
}
}
if summary.StartedAt != nil && summary.CompletedAt != nil && !summary.CompletedAt.Before(*summary.StartedAt) {
summary.DurationMs = summary.CompletedAt.Sub(*summary.StartedAt).Milliseconds()
}
if total == 0 {
return summary, nil
}