Skip to content

Commit ce80db9

Browse files
committed
fix(console): clear chat history on /clear
1 parent 1c5c6cc commit ce80db9

4 files changed

Lines changed: 110 additions & 4 deletions

File tree

console/src/pages/Chat/index.tsx

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,46 @@ interface CommandSuggestion {
6161
description: string;
6262
}
6363

64+
function messageRequestsHistoryClear(message: unknown): boolean {
65+
if (!message || typeof message !== "object") return false;
66+
const metadata = (message as Record<string, unknown>).metadata;
67+
if (!metadata || typeof metadata !== "object") return false;
68+
69+
const meta = metadata as Record<string, unknown>;
70+
if (meta.clear_history === true) return true;
71+
72+
const nested = meta.metadata;
73+
return (
74+
!!nested &&
75+
typeof nested === "object" &&
76+
(nested as Record<string, unknown>).clear_history === true
77+
);
78+
}
79+
80+
function payloadRequestsHistoryClear(payload: unknown): boolean {
81+
if (!payload || typeof payload !== "object") return false;
82+
83+
const record = payload as Record<string, unknown>;
84+
const candidates: unknown[] = [];
85+
86+
if (record.object === "message") {
87+
candidates.push(record);
88+
}
89+
90+
if (record.object === "response" && Array.isArray(record.output)) {
91+
candidates.push(...record.output);
92+
}
93+
94+
return candidates.some(messageRequestsHistoryClear);
95+
}
96+
97+
function payloadCompletesResponse(payload: unknown): boolean {
98+
if (!payload || typeof payload !== "object") return false;
99+
100+
const record = payload as Record<string, unknown>;
101+
return record.object === "response" && record.status === "completed";
102+
}
103+
64104
function renderSuggestionLabel(command: string, description: string) {
65105
return (
66106
<div className={styles.suggestionLabel}>
@@ -433,11 +473,20 @@ export default function ChatPage() {
433473
const chatIdRef = useRef(chatId);
434474
const navigateRef = useRef(navigate);
435475
const chatRef = useRef<IAgentScopeRuntimeWebUIRef>(null);
476+
const pendingClearHistoryRef = useRef(false);
436477

437478
useMessageHistoryNavigation(chatRef, isChatActive, isComposingRef);
438479
chatIdRef.current = chatId;
439480
navigateRef.current = navigate;
440481

482+
const scheduleHistoryClear = useCallback(() => {
483+
queueMicrotask(() => {
484+
if (!pendingClearHistoryRef.current) return;
485+
pendingClearHistoryRef.current = false;
486+
chatRef.current?.messages.removeAllMessages();
487+
});
488+
}, []);
489+
441490
// Tell sessionApi which session to put first in getSessionList, so the library's
442491
// useMount auto-selects the correct session without an extra getSession round-trip.
443492
if (chatId && sessionApi.preferredChatId !== chatId) {
@@ -777,6 +826,16 @@ export default function ChatPage() {
777826
api: {
778827
...defaultConfig.api,
779828
fetch: customFetch,
829+
responseParser: (chunk: string) => {
830+
const payload = JSON.parse(chunk) as Record<string, unknown>;
831+
if (payloadRequestsHistoryClear(payload)) {
832+
pendingClearHistoryRef.current = true;
833+
if (payloadCompletesResponse(payload)) {
834+
scheduleHistoryClear();
835+
}
836+
}
837+
return payload as any;
838+
},
780839
replaceMediaURL: (url: string) => {
781840
return toDisplayUrl(url);
782841
},
@@ -824,7 +883,15 @@ export default function ChatPage() {
824883
replace: true,
825884
},
826885
} as unknown as IAgentScopeRuntimeWebUIOptions;
827-
}, [customFetch, copyResponse, handleFileUpload, t, isDark, multimodalCaps]);
886+
}, [
887+
customFetch,
888+
copyResponse,
889+
handleFileUpload,
890+
t,
891+
isDark,
892+
multimodalCaps,
893+
scheduleHistoryClear,
894+
]);
828895

829896
return (
830897
<div

console/src/pages/Chat/sessionApi/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ interface ContentItem {
5151
/** A backend message after role-normalisation (output of toOutputMessage). */
5252
interface OutputMessage extends Omit<Message, "role"> {
5353
role: string;
54-
metadata: null;
54+
metadata: unknown;
5555
sequence_number?: number;
5656
}
5757

@@ -155,7 +155,7 @@ const toOutputMessage = (msg: Message): OutputMessage => ({
155155
msg.type === TYPE_PLUGIN_CALL_OUTPUT && msg.role === "system"
156156
? ROLE_TOOL
157157
: msg.role,
158-
metadata: null,
158+
metadata: msg.metadata ?? null,
159159
});
160160

161161
/** Build a user card (AgentScopeRuntimeRequestCard) from a user message. */

src/qwenpaw/agents/command_handler.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,16 @@ def is_command(self, query: str | None) -> bool:
9494
"""Check if the query is a system command (alias for mixin)."""
9595
return self.is_conversation_command(query)
9696

97-
async def _make_system_msg(self, text: str) -> Msg:
97+
async def _make_system_msg(
98+
self,
99+
text: str,
100+
metadata: dict | None = None,
101+
) -> Msg:
98102
"""Create a system response message.
99103
100104
Args:
101105
text: Message text content
106+
metadata: Optional structured metadata for downstream consumers
102107
103108
Returns:
104109
System message
@@ -107,6 +112,7 @@ async def _make_system_msg(self, text: str) -> Msg:
107112
name=self.agent_name,
108113
role="assistant",
109114
content=[TextBlock(type="text", text=text)],
115+
metadata=metadata or {},
110116
)
111117

112118
def _has_memory_manager(self) -> bool:
@@ -198,6 +204,7 @@ async def _process_clear(
198204
"**History Cleared!**\n\n"
199205
"- Compressed summary reset\n"
200206
"- Memory is now empty",
207+
metadata={"clear_history": True},
201208
)
202209

203210
async def _process_compact_str(
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# -*- coding: utf-8 -*-
2+
import pytest
3+
4+
from qwenpaw.agents.command_handler import CommandHandler
5+
6+
7+
class DummyMemory:
8+
def __init__(self) -> None:
9+
self.clear_content_called = 0
10+
self.clear_summary_called = 0
11+
12+
def clear_content(self) -> None:
13+
self.clear_content_called += 1
14+
15+
def clear_compressed_summary(self) -> None:
16+
self.clear_summary_called += 1
17+
18+
async def get_memory(self, prepend_summary: bool = False) -> list:
19+
assert prepend_summary is False
20+
return []
21+
22+
23+
@pytest.mark.asyncio
24+
async def test_process_clear_returns_clear_history_metadata() -> None:
25+
memory = DummyMemory()
26+
handler = CommandHandler(agent_name="QwenPaw", memory=memory)
27+
28+
msg = await handler.handle_command("/clear")
29+
30+
assert memory.clear_content_called == 1
31+
assert memory.clear_summary_called == 1
32+
assert msg.metadata == {"clear_history": True}

0 commit comments

Comments
 (0)