mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-29 22:18:46 +00:00
fix: coalesce repeated idle TUI abort notices (#85167)
This commit is contained in:
@@ -62,6 +62,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Node/Linux: keep `OPENCLAW_GATEWAY_TOKEN` out of generated systemd unit files by writing node service token values to a node-specific env file. (#84408)
|
||||
- Memory-core/dreaming: reuse stable narrative subagent session keys per workspace and phase while keeping per-run idempotency and bounded cleanup, so stale `dreaming-narrative-*` sessions do not accumulate. Fixes #68252, #69187, and #70402. (#70464) Thanks @chiyouYCH.
|
||||
- Trajectory/support: tolerate partial skill snapshot entries when building support metadata so rejected skill path scans no longer abort trajectory capture. (#71185) Thanks @lukeboyett.
|
||||
- TUI: coalesce repeated idle Esc abort notices into a single `no active run xN` system row instead of appending duplicate rows.
|
||||
- Telegram: honor `channels.telegram.pollingStallThresholdMs` in the default isolated polling path, restarting silent workers instead of leaving inbound updates wedged. Fixes #83950. (#84861) Thanks @joshavant.
|
||||
- Slack: suppress reasoning payloads before reply delivery and dispatch accounting, so Slack monitor, slash-command, fallback, and direct reply paths do not leak model reasoning. Fixes #84319. (#84322) Thanks @ffluk3 and @joshavant.
|
||||
- Slack: deliver native plugin approval prompts and updates when Slack native approvals are enabled, while keeping plugin approval authorization separate from exec approvers.
|
||||
|
||||
@@ -15,6 +15,39 @@ describe("ChatLog", () => {
|
||||
expect(rendered).not.toContain("system-1");
|
||||
});
|
||||
|
||||
it("coalesces consecutive repeatable system messages", () => {
|
||||
const chatLog = new ChatLog(20);
|
||||
|
||||
chatLog.addSystem("no active run", { coalesceConsecutive: true });
|
||||
chatLog.addSystem("no active run", { coalesceConsecutive: true });
|
||||
chatLog.addSystem("no active run", { coalesceConsecutive: true });
|
||||
|
||||
const rendered = normalizeTestText(chatLog.render(120).join("\n"));
|
||||
expect(chatLog.children.length).toBe(1);
|
||||
expect(rendered).toContain("no active run x3");
|
||||
});
|
||||
|
||||
it("does not coalesce ordinary system messages", () => {
|
||||
const chatLog = new ChatLog(20);
|
||||
|
||||
chatLog.addSystem("status unchanged");
|
||||
chatLog.addSystem("status unchanged");
|
||||
|
||||
expect(chatLog.children.length).toBe(2);
|
||||
});
|
||||
|
||||
it("starts a new repeatable system message after other chat content", () => {
|
||||
const chatLog = new ChatLog(20);
|
||||
|
||||
chatLog.addSystem("no active run", { coalesceConsecutive: true });
|
||||
chatLog.addUser("hello");
|
||||
chatLog.addSystem("no active run", { coalesceConsecutive: true });
|
||||
|
||||
const rendered = normalizeTestText(chatLog.render(120).join("\n"));
|
||||
expect(chatLog.children.length).toBe(3);
|
||||
expect(rendered).not.toContain("no active run x2");
|
||||
});
|
||||
|
||||
it("drops stale streaming references when old components are pruned", () => {
|
||||
const chatLog = new ChatLog(20);
|
||||
chatLog.startAssistant("first", "run-1");
|
||||
|
||||
@@ -8,6 +8,13 @@ import { UserMessageComponent } from "./user-message.js";
|
||||
|
||||
const PENDING_HISTORY_CLOCK_SKEW_TOLERANCE_MS = 60_000;
|
||||
|
||||
type RepeatableSystemMessage = {
|
||||
component: Container;
|
||||
textNode: Text;
|
||||
baseText: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export class ChatLog extends Container {
|
||||
private readonly maxComponents: number;
|
||||
private toolById = new Map<string, ToolExecutionComponent>();
|
||||
@@ -22,6 +29,7 @@ export class ChatLog extends Container {
|
||||
>();
|
||||
private btwMessage: BtwInlineMessage | null = null;
|
||||
private toolsExpanded = false;
|
||||
private repeatableSystemMessage: RepeatableSystemMessage | null = null;
|
||||
|
||||
constructor(maxComponents = 180) {
|
||||
super();
|
||||
@@ -47,6 +55,9 @@ export class ChatLog extends Container {
|
||||
if (this.btwMessage === component) {
|
||||
this.btwMessage = null;
|
||||
}
|
||||
if (this.repeatableSystemMessage?.component === component) {
|
||||
this.repeatableSystemMessage = null;
|
||||
}
|
||||
}
|
||||
|
||||
private pruneOverflow() {
|
||||
@@ -65,11 +76,17 @@ export class ChatLog extends Container {
|
||||
this.pruneOverflow();
|
||||
}
|
||||
|
||||
private appendNonSystem(component: Component) {
|
||||
this.repeatableSystemMessage = null;
|
||||
this.append(component);
|
||||
}
|
||||
|
||||
clearAll(opts?: { preservePendingUsers?: boolean }) {
|
||||
this.clear();
|
||||
this.toolById.clear();
|
||||
this.streamingRuns.clear();
|
||||
this.btwMessage = null;
|
||||
this.repeatableSystemMessage = null;
|
||||
if (!opts?.preservePendingUsers) {
|
||||
this.pendingUsers.clear();
|
||||
}
|
||||
@@ -80,7 +97,7 @@ export class ChatLog extends Container {
|
||||
if (this.children.includes(entry.component)) {
|
||||
continue;
|
||||
}
|
||||
this.append(entry.component);
|
||||
this.appendNonSystem(entry.component);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,19 +108,42 @@ export class ChatLog extends Container {
|
||||
this.pendingUsers.clear();
|
||||
}
|
||||
|
||||
private createSystemMessage(text: string): Container {
|
||||
const entry = new Container();
|
||||
entry.addChild(new Spacer(1));
|
||||
entry.addChild(new Text(theme.system(text), 1, 0));
|
||||
return entry;
|
||||
private formatRepeatedSystemText(text: string, count: number) {
|
||||
return count > 1 ? `${text} x${count}` : text;
|
||||
}
|
||||
|
||||
addSystem(text: string) {
|
||||
this.append(this.createSystemMessage(text));
|
||||
private createSystemMessage(text: string): RepeatableSystemMessage {
|
||||
const entry = new Container();
|
||||
const textNode = new Text(theme.system(text), 1, 0);
|
||||
entry.addChild(new Spacer(1));
|
||||
entry.addChild(textNode);
|
||||
return {
|
||||
component: entry,
|
||||
textNode,
|
||||
baseText: text,
|
||||
count: 1,
|
||||
};
|
||||
}
|
||||
|
||||
addSystem(text: string, opts?: { coalesceConsecutive?: boolean }) {
|
||||
if (
|
||||
opts?.coalesceConsecutive &&
|
||||
this.repeatableSystemMessage?.baseText === text &&
|
||||
this.children[this.children.length - 1] === this.repeatableSystemMessage.component
|
||||
) {
|
||||
this.repeatableSystemMessage.count += 1;
|
||||
this.repeatableSystemMessage.textNode.setText(
|
||||
theme.system(this.formatRepeatedSystemText(text, this.repeatableSystemMessage.count)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const message = this.createSystemMessage(text);
|
||||
this.append(message.component);
|
||||
this.repeatableSystemMessage = opts?.coalesceConsecutive ? message : null;
|
||||
}
|
||||
|
||||
addUser(text: string) {
|
||||
this.append(new UserMessageComponent(text));
|
||||
this.appendNonSystem(new UserMessageComponent(text));
|
||||
}
|
||||
|
||||
addPendingUser(runId: string, text: string, createdAt = Date.now()) {
|
||||
@@ -116,7 +156,7 @@ export class ChatLog extends Container {
|
||||
}
|
||||
const component = new UserMessageComponent(text);
|
||||
this.pendingUsers.set(runId, { component, text, createdAt });
|
||||
this.append(component);
|
||||
this.appendNonSystem(component);
|
||||
return component;
|
||||
}
|
||||
|
||||
@@ -192,7 +232,7 @@ export class ChatLog extends Container {
|
||||
}
|
||||
const component = new AssistantMessageComponent(text);
|
||||
this.streamingRuns.set(effectiveRunId, component);
|
||||
this.append(component);
|
||||
this.appendNonSystem(component);
|
||||
return component;
|
||||
}
|
||||
|
||||
@@ -214,7 +254,7 @@ export class ChatLog extends Container {
|
||||
this.streamingRuns.delete(effectiveRunId);
|
||||
return;
|
||||
}
|
||||
this.append(new AssistantMessageComponent(text));
|
||||
this.appendNonSystem(new AssistantMessageComponent(text));
|
||||
}
|
||||
|
||||
dropAssistant(runId?: string) {
|
||||
@@ -232,13 +272,13 @@ export class ChatLog extends Container {
|
||||
this.btwMessage.setResult(params);
|
||||
if (this.children[this.children.length - 1] !== this.btwMessage) {
|
||||
this.removeChild(this.btwMessage);
|
||||
this.append(this.btwMessage);
|
||||
this.appendNonSystem(this.btwMessage);
|
||||
}
|
||||
return this.btwMessage;
|
||||
}
|
||||
const component = new BtwInlineMessage(params);
|
||||
this.btwMessage = component;
|
||||
this.append(component);
|
||||
this.appendNonSystem(component);
|
||||
return component;
|
||||
}
|
||||
|
||||
@@ -263,7 +303,7 @@ export class ChatLog extends Container {
|
||||
const component = new ToolExecutionComponent(toolName, args);
|
||||
component.setExpanded(this.toolsExpanded);
|
||||
this.toolById.set(toolCallId, component);
|
||||
this.append(component);
|
||||
this.appendNonSystem(component);
|
||||
return component;
|
||||
}
|
||||
|
||||
|
||||
@@ -386,6 +386,26 @@ describe("tui session actions", () => {
|
||||
expect(setActivityStatus).toHaveBeenCalledWith("aborted");
|
||||
});
|
||||
|
||||
it("coalesces repeated no-active-run abort notices", async () => {
|
||||
const addSystem = vi.fn();
|
||||
const requestRender = vi.fn();
|
||||
|
||||
const { abortActive } = createTestSessionActions({
|
||||
chatLog: {
|
||||
addSystem,
|
||||
clearAll: vi.fn(),
|
||||
} as unknown as import("./components/chat-log.js").ChatLog,
|
||||
tui: { requestRender } as unknown as import("@earendil-works/pi-tui").TUI,
|
||||
});
|
||||
|
||||
await abortActive();
|
||||
|
||||
expect(addSystem).toHaveBeenCalledWith("no active run", {
|
||||
coalesceConsecutive: true,
|
||||
});
|
||||
expect(requestRender).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("remembers the selected session after history loads", async () => {
|
||||
const listSessions = vi.fn().mockResolvedValue({
|
||||
ts: Date.now(),
|
||||
|
||||
@@ -397,7 +397,7 @@ export function createSessionActions(context: SessionActionContext) {
|
||||
const abortActive = async () => {
|
||||
const runId = state.activeChatRunId ?? state.pendingChatRunId ?? null;
|
||||
if (!runId) {
|
||||
chatLog.addSystem("no active run");
|
||||
chatLog.addSystem("no active run", { coalesceConsecutive: true });
|
||||
tui.requestRender();
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user