diff --git a/Cargo.lock b/Cargo.lock
index 057944c..507e911 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1728,7 +1728,7 @@ dependencies = [
[[package]]
name = "picobot"
-version = "0.3.3"
+version = "0.3.5"
dependencies = [
"anyhow",
"async-trait",
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 4ecd51d..132ebea 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -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 而非 0(0 表示不超时,与用户预期不符)。
- 新增两个 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 单元测试。
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 6ea51c6..07739f1 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -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 (
@@ -906,9 +906,9 @@ function App() {
)}
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}
/>
);
diff --git a/web/src/components/Chat/MessageInput.tsx b/web/src/components/Chat/MessageInput.tsx
index 87478e2..df97dd9 100644
--- a/web/src/components/Chat/MessageInput.tsx
+++ b/web/src/components/Chat/MessageInput.tsx
@@ -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
(null);
const fileInputRef = useRef(null);
const wasLoadingRef = useRef(false);
+ const prevTopicIdRef = useRef(topicId);
+
+ // 切换话题时清空草稿(替代原来通过 key remount 的重置机制)
+ useEffect(() => {
+ if (prevTopicIdRef.current !== topicId) {
+ prevTopicIdRef.current = topicId;
+ setContent('');
+ setAttachments([]);
+ }
+ }, [topicId]);
useEffect(() => {
const textarea = textareaRef.current;