* fix: prevent duplicate assistant messages in TUI (fixes #35278) When startAssistant() is called multiple times with the same runId, it was creating duplicate AssistantMessageComponent instances instead of reusing the existing one. This caused messages to appear twice in the terminal UI. The fix checks if a component already exists for the runId before creating a new one. If it exists, we update its text instead of appending a duplicate component. Test coverage includes verification that: - Only one component is created when startAssistant is called twice - The second text replaces the first - Component count remains 1 (prevents regression) Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> * Changelog: add TUI duplicate-render fix entry --------- Co-authored-by: 沐沐 <mumu@example.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Happy <yesreply@happy.engineering> Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- TUI/chat log: reuse the active assistant message component for the same streaming run so `openclaw tui` no longer renders duplicate assistant replies. (#35364) Thanks @lisitan.
|
||||
- macOS/Reminders: add the missing `NSRemindersUsageDescription` to the bundled app so `apple-reminders` can trigger the system permission prompt from OpenClaw.app. (#8559) Thanks @dinakars777.
|
||||
- iMessage/self-chat echo dedupe: drop reflected duplicate copies only when a matching `is_from_me` event was just seen for the same chat, text, and `created_at`, preventing self-chat loops without broad text-only suppression. Related to #32166. (#38440) Thanks @vincentkoc.
|
||||
|
||||
|
||||
@@ -29,6 +29,17 @@ describe("ChatLog", () => {
|
||||
expect(rendered).toContain("recreated");
|
||||
});
|
||||
|
||||
it("does not append duplicate assistant components when a run is started twice", () => {
|
||||
const chatLog = new ChatLog(40);
|
||||
chatLog.startAssistant("first", "run-dup");
|
||||
chatLog.startAssistant("second", "run-dup");
|
||||
|
||||
const rendered = chatLog.render(120).join("\n");
|
||||
expect(rendered).toContain("second");
|
||||
expect(rendered).not.toContain("first");
|
||||
expect(chatLog.children.length).toBe(1);
|
||||
});
|
||||
|
||||
it("drops stale tool references when old components are pruned", () => {
|
||||
const chatLog = new ChatLog(20);
|
||||
chatLog.startTool("tool-1", "read_file", { path: "a.txt" });
|
||||
|
||||
@@ -65,8 +65,14 @@ export class ChatLog extends Container {
|
||||
}
|
||||
|
||||
startAssistant(text: string, runId?: string) {
|
||||
const effectiveRunId = this.resolveRunId(runId);
|
||||
const existing = this.streamingRuns.get(effectiveRunId);
|
||||
if (existing) {
|
||||
existing.setText(text);
|
||||
return existing;
|
||||
}
|
||||
const component = new AssistantMessageComponent(text);
|
||||
this.streamingRuns.set(this.resolveRunId(runId), component);
|
||||
this.streamingRuns.set(effectiveRunId, component);
|
||||
this.append(component);
|
||||
return component;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user