fix(web): 移除 ChatContainer 的 key remount,修复新建话题列表刷新延迟

话题切换时 key={selectedTopic} 导致整个 ChatContainer 子树卸载重建,
触发 ExpertSelector/ModelSelector 重新挂载并发起 3 次冗余 HTTP 请求
(getSelectedExpert/listModelOptions/模型选择刷新),引入 200-500ms 延迟。

改用 topicId prop 透传话题 ID:
- App.tsx: 移除 key,viewKey 纳入 selectedTopic 保持各话题独立滚动位置
- ChatContainer: 新增 topicId prop 透传给 MessageInput
- MessageInput: 监听 topicId 变化清空草稿,替代原 key remount 重置机制
This commit is contained in:
oudecheng 2026-08-12 09:13:48 +08:00
parent 3910bc324b
commit a9ce308c05
5 changed files with 29 additions and 5 deletions

2
Cargo.lock generated
View File

@ -1728,7 +1728,7 @@ dependencies = [
[[package]]
name = "picobot"
version = "0.3.3"
version = "0.3.5"
dependencies = [
"anyhow",
"async-trait",

View File

@ -4,7 +4,7 @@
## [0.3.5] - 2026-08-12
较 [0.3.4] 的 4 个 commit 迭代,聚焦 **可观测性**、**并发性能** 与 **外部集成健壮性** 三大方向。
较 [0.3.4] 的 5 个 commit 迭代,聚焦 **可观测性**、**并发性能** 与 **外部集成健壮性** 三大方向。
### 新增功能
@ -36,6 +36,13 @@
- 清空输入框时回退到默认值 300 而非 00 表示不超时,与用户预期不符)。
- 新增两个 tokio 测试覆盖有超时和无超时error pass-through路径。
#### 移除 ChatContainer key remount 修复新建话题列表刷新延迟
- 话题切换时 `key={selectedTopic}` 导致整个 `ChatContainer` 子树卸载重建,触发 `ExpertSelector` / `ModelSelector` 重新挂载并发起 3 次冗余 HTTP 请求(`getSelectedExpert` / `listModelOptions` / 模型选择刷新),引入 200-500ms 延迟。
- 改用 `topicId` prop 透传话题 ID
- `App.tsx`:移除 key`viewKey` 纳入 `selectedTopic` 保持各话题独立滚动位置。
- `ChatContainer`:新增 `topicId` prop 透传给 `MessageInput`
- `MessageInput`:监听 `topicId` 变化清空草稿,替代原 key remount 重置机制。
### 测试
- MCP 超时路径新增 2 个 tokio 单元测试。

View File

@ -611,8 +611,8 @@ function App() {
const viewKey = useMemo(() => {
if (schedulerView) return `scheduler:${schedulerView.jobId}`;
if (subAgentView) return `subagent:${subAgentView.taskId}`;
return 'main';
}, [schedulerView, subAgentView]);
return `main:${selectedTopic ?? ''}`;
}, [schedulerView, subAgentView, selectedTopic]);
return (
<div className="flex h-screen flex-col bg-[var(--bg-primary)] text-[var(--text-primary)] overflow-hidden">
@ -906,9 +906,9 @@ function App() {
)}
<div className="flex-1 min-h-0">
<ChatContainer
key={selectedTopic ?? 'no-topic'}
messages={chatMessages}
isLoading={isLoading}
topicId={selectedTopic}
isReadOnly={subAgentView || schedulerView ? true : isReadOnly}
channelName={
schedulerView

View File

@ -24,6 +24,8 @@ interface ChatContainerProps {
onOpenSettings?: () => void;
/** 设置弹窗关闭信号(每次关闭递增,用于触发 ExpertSelector 刷新) */
settingsClosedTick?: number;
/** 当前话题 ID用于切换话题时清空输入框草稿 */
topicId?: string | null;
}
export function ChatContainer({
@ -40,6 +42,7 @@ export function ChatContainer({
sessionId,
onOpenSettings,
settingsClosedTick,
topicId,
}: ChatContainerProps) {
const [selectedExpert, setSelectedExpert] = useState<{
name: string;
@ -74,6 +77,7 @@ export function ChatContainer({
isReadOnly={isReadOnly}
channelName={channelName}
selectedExpert={selectedExpert}
topicId={topicId}
/>
</div>
);

View File

@ -25,6 +25,8 @@ interface MessageInputProps {
isReadOnly?: boolean;
channelName?: string;
selectedExpert?: { name: string; description: string } | null;
/** 当前话题 ID切换话题时自动清空草稿 */
topicId?: string | null;
}
interface FileAttachment {
@ -51,6 +53,7 @@ export function MessageInput({
isReadOnly = false,
channelName,
selectedExpert,
topicId,
}: MessageInputProps) {
const effectivePlaceholder =
placeholder ??
@ -62,6 +65,16 @@ export function MessageInput({
const textareaRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const wasLoadingRef = useRef(false);
const prevTopicIdRef = useRef<string | null | undefined>(topicId);
// 切换话题时清空草稿(替代原来通过 key remount 的重置机制)
useEffect(() => {
if (prevTopicIdRef.current !== topicId) {
prevTopicIdRef.current = topicId;
setContent('');
setAttachments([]);
}
}, [topicId]);
useEffect(() => {
const textarea = textareaRef.current;