mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 15:30:39 +00:00
TUI: bound chat-log growth to prevent render overflows
This commit is contained in:
@@ -65,6 +65,7 @@ Docs: https://docs.openclaw.ai
|
||||
- TUI/Pairing: show explicit pairing-required recovery guidance after gateway disconnects that return `pairing required`, including approval steps to unblock quickstart TUI hatching on fresh installs. (#21841) Thanks @nicolinux.
|
||||
- TUI/Input: suppress duplicate backspace events arriving in the same input burst window so SSH sessions no longer delete two characters per backspace press in the composer. (#19318) Thanks @eheimer.
|
||||
- TUI/Heartbeat: suppress heartbeat ACK/prompt noise in chat streaming when `showOk` is disabled, while still preserving non-ACK heartbeat alerts in final output. (#20228) Thanks @bhalliburton.
|
||||
- TUI/History: cap chat-log component growth and prune stale render nodes/references so large default history loads no longer overflow render recursion with `RangeError: Maximum call stack size exceeded`. (#18068) Thanks @JaniJegoroff.
|
||||
- Memory/QMD: diversify mixed-source search ranking when both session and memory collections are present so session transcript hits no longer crowd out durable memory-file matches in top results. (#19913) Thanks @alextempr.
|
||||
- Auth/Onboarding: align OAuth profile-id config mapping with stored credential IDs for OpenAI Codex and Chutes flows, preventing `provider:default` mismatches when OAuth returns email-scoped credentials. (#12692) thanks @mudrii.
|
||||
- Docker: pin base images to SHA256 digests in Docker builds to prevent mutable tag drift. (#7734) Thanks @coygeek.
|
||||
|
||||
44
src/tui/components/chat-log.test.ts
Normal file
44
src/tui/components/chat-log.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ChatLog } from "./chat-log.js";
|
||||
|
||||
describe("ChatLog", () => {
|
||||
it("caps component growth to avoid unbounded render trees", () => {
|
||||
const chatLog = new ChatLog(20);
|
||||
for (let i = 1; i <= 40; i++) {
|
||||
chatLog.addSystem(`system-${i}`);
|
||||
}
|
||||
|
||||
expect(chatLog.children.length).toBe(20);
|
||||
const rendered = chatLog.render(120).join("\n");
|
||||
expect(rendered).toContain("system-40");
|
||||
expect(rendered).not.toContain("system-1");
|
||||
});
|
||||
|
||||
it("drops stale streaming references when old components are pruned", () => {
|
||||
const chatLog = new ChatLog(20);
|
||||
chatLog.startAssistant("first", "run-1");
|
||||
for (let i = 0; i < 25; i++) {
|
||||
chatLog.addSystem(`overflow-${i}`);
|
||||
}
|
||||
|
||||
// Should not throw if the original streaming component was pruned.
|
||||
chatLog.updateAssistant("recreated", "run-1");
|
||||
|
||||
const rendered = chatLog.render(120).join("\n");
|
||||
expect(chatLog.children.length).toBe(20);
|
||||
expect(rendered).toContain("recreated");
|
||||
});
|
||||
|
||||
it("drops stale tool references when old components are pruned", () => {
|
||||
const chatLog = new ChatLog(20);
|
||||
chatLog.startTool("tool-1", "read_file", { path: "a.txt" });
|
||||
for (let i = 0; i < 25; i++) {
|
||||
chatLog.addSystem(`overflow-${i}`);
|
||||
}
|
||||
|
||||
// Should no-op safely after the tool component is pruned.
|
||||
chatLog.updateToolResult("tool-1", { content: [{ type: "text", text: "done" }] });
|
||||
|
||||
expect(chatLog.children.length).toBe(20);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Component } from "@mariozechner/pi-tui";
|
||||
import { Container, Spacer, Text } from "@mariozechner/pi-tui";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { AssistantMessageComponent } from "./assistant-message.js";
|
||||
@@ -5,10 +6,45 @@ import { ToolExecutionComponent } from "./tool-execution.js";
|
||||
import { UserMessageComponent } from "./user-message.js";
|
||||
|
||||
export class ChatLog extends Container {
|
||||
private readonly maxComponents: number;
|
||||
private toolById = new Map<string, ToolExecutionComponent>();
|
||||
private streamingRuns = new Map<string, AssistantMessageComponent>();
|
||||
private toolsExpanded = false;
|
||||
|
||||
constructor(maxComponents = 180) {
|
||||
super();
|
||||
this.maxComponents = Math.max(20, Math.floor(maxComponents));
|
||||
}
|
||||
|
||||
private dropComponentReferences(component: Component) {
|
||||
for (const [toolId, tool] of this.toolById.entries()) {
|
||||
if (tool === component) {
|
||||
this.toolById.delete(toolId);
|
||||
}
|
||||
}
|
||||
for (const [runId, message] of this.streamingRuns.entries()) {
|
||||
if (message === component) {
|
||||
this.streamingRuns.delete(runId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private pruneOverflow() {
|
||||
while (this.children.length > this.maxComponents) {
|
||||
const oldest = this.children[0];
|
||||
if (!oldest) {
|
||||
return;
|
||||
}
|
||||
this.removeChild(oldest);
|
||||
this.dropComponentReferences(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
private append(component: Component) {
|
||||
this.addChild(component);
|
||||
this.pruneOverflow();
|
||||
}
|
||||
|
||||
clearAll() {
|
||||
this.clear();
|
||||
this.toolById.clear();
|
||||
@@ -16,12 +52,12 @@ export class ChatLog extends Container {
|
||||
}
|
||||
|
||||
addSystem(text: string) {
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new Text(theme.system(text), 1, 0));
|
||||
this.append(new Spacer(1));
|
||||
this.append(new Text(theme.system(text), 1, 0));
|
||||
}
|
||||
|
||||
addUser(text: string) {
|
||||
this.addChild(new UserMessageComponent(text));
|
||||
this.append(new UserMessageComponent(text));
|
||||
}
|
||||
|
||||
private resolveRunId(runId?: string) {
|
||||
@@ -31,7 +67,7 @@ export class ChatLog extends Container {
|
||||
startAssistant(text: string, runId?: string) {
|
||||
const component = new AssistantMessageComponent(text);
|
||||
this.streamingRuns.set(this.resolveRunId(runId), component);
|
||||
this.addChild(component);
|
||||
this.append(component);
|
||||
return component;
|
||||
}
|
||||
|
||||
@@ -53,7 +89,7 @@ export class ChatLog extends Container {
|
||||
this.streamingRuns.delete(effectiveRunId);
|
||||
return;
|
||||
}
|
||||
this.addChild(new AssistantMessageComponent(text));
|
||||
this.append(new AssistantMessageComponent(text));
|
||||
}
|
||||
|
||||
dropAssistant(runId?: string) {
|
||||
@@ -75,7 +111,7 @@ export class ChatLog extends Container {
|
||||
const component = new ToolExecutionComponent(toolName, args);
|
||||
component.setExpanded(this.toolsExpanded);
|
||||
this.toolById.set(toolCallId, component);
|
||||
this.addChild(component);
|
||||
this.append(component);
|
||||
return component;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user