Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions web/src/contexts/AgentContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ export interface ChatMessage {
markdown?: boolean;
toolCall?: ToolCallInfo;
timestamp: Date;
/**
* Locally-generated info/system message produced by web slash-command
* handlers (`/help`, `/model`, unknown-command notices). Excluded from
* persistence so command output does not pollute localStorage and reappear
* as fake assistant replies on reload. See #7137.
*/
local?: boolean;
}

interface AgentContextValue {
Expand All @@ -40,6 +47,13 @@ interface AgentContextValue {
refreshModels: () => void;
deleteMessage: (id: string) => void;
clearAllMessages: () => void;
/**
* Append a locally-generated info/system message to the transcript without
* sending anything to the gateway. Used by web slash-command handlers
* (`/help`, `/model`, unknown-command notices) to surface feedback inline.
* See #7137.
*/
addLocalMessage: (content: string) => void;
abortSession: () => Promise<void>;
/**
* Pending supervised-mode tool-approval prompt, or null. Populated when the
Expand Down Expand Up @@ -723,6 +737,21 @@ export function AgentProvider({ agentAlias, children }: AgentProviderProps) {
})();
}, [agentAlias, attachSocketCallbacks]);

const addLocalMessage = useCallback((content: string) => {
localMessageMutationVersionRef.current += 1;
setMessages((prev) => [
...prev,
{
id: generateUUID(),
role: 'agent',
content,
markdown: true,
timestamp: new Date(),
local: true,
},
]);
}, []);

const respondToApproval = useCallback((decision: ApprovalDecision) => {
setPendingApproval((current) => {
if (!current) return null;
Expand Down Expand Up @@ -750,6 +779,7 @@ export function AgentProvider({ agentAlias, children }: AgentProviderProps) {
refreshModels: () => setModelInfoVersion((v) => v + 1),
deleteMessage,
clearAllMessages,
addLocalMessage,
abortSession: async () => {
// Clear local approval state immediately — the in-flight request_id
// belongs to the turn we're cancelling and will be rejected by the
Expand Down
24 changes: 15 additions & 9 deletions web/src/lib/chatHistoryStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,21 @@ export function uiMessagesToPersisted(
markdown?: boolean;
toolCall?: { name: string; args?: unknown; output?: string };
timestamp: Date;
local?: boolean;
}>,
): PersistedChatBubble[] {
return messages.map((m) => ({
id: m.id,
role: m.role,
content: m.content,
thinking: m.thinking,
markdown: m.markdown,
toolCall: m.toolCall,
timestamp: m.timestamp.toISOString(),
}));
return messages
// Skip messages flagged `local: true` (web slash-command output like /help,
// /model banners, unknown-command notices). They are ephemeral UI feedback
// and must not be re-hydrated as fake assistant replies on reload. #7137
.filter((m) => !m.local)
.map((m) => ({
id: m.id,
role: m.role,
content: m.content,
thinking: m.thinking,
markdown: m.markdown,
toolCall: m.toolCall,
timestamp: m.timestamp.toISOString(),
}));
}
Loading