diff --git a/desktop/src/stores/chatStore.test.ts b/desktop/src/stores/chatStore.test.ts
index d00698d1..b075eaa0 100644
--- a/desktop/src/stores/chatStore.test.ts
+++ b/desktop/src/stores/chatStore.test.ts
@@ -463,6 +463,50 @@ describe('chatStore history mapping', () => {
])
})
+ it('does not restore internal slash-command breadcrumbs as user history bubbles', () => {
+ const messages: MessageEntry[] = [
+ {
+ id: 'agent-command-string',
+ type: 'user',
+ timestamp: '2026-06-15T03:32:13.000Z',
+ content: [
+ 'agent',
+ '/agent',
+ 'Plan 222',
+ ].join('\n'),
+ },
+ {
+ id: 'agent-command-array',
+ type: 'user',
+ timestamp: '2026-06-15T03:32:14.000Z',
+ content: [
+ {
+ type: 'text',
+ text: [
+ 'agent',
+ '/agent',
+ 'Plan 333',
+ ].join('\n'),
+ },
+ ],
+ },
+ {
+ id: 'transcript-user-1',
+ type: 'user',
+ timestamp: '2026-06-15T03:32:15.000Z',
+ content: '继续处理这个问题',
+ },
+ ]
+
+ expect(mapHistoryMessagesToUiMessages(messages)).toMatchObject([
+ {
+ id: 'transcript-user-1',
+ type: 'user_text',
+ content: '继续处理这个问题',
+ },
+ ])
+ })
+
it('restores persisted image user messages as renderable attachments without exposing image metadata text', () => {
const messages: MessageEntry[] = [
{
diff --git a/desktop/src/stores/chatStore.ts b/desktop/src/stores/chatStore.ts
index e72d9e85..68551830 100644
--- a/desktop/src/stores/chatStore.ts
+++ b/desktop/src/stores/chatStore.ts
@@ -2316,6 +2316,16 @@ function extractHistoryTextBlocks(content: unknown): string[] {
.filter(Boolean)
}
+function isInternalCommandBreadcrumbContent(content: unknown): boolean {
+ const textBlocks = extractHistoryTextBlocks(content)
+ return textBlocks.length > 0 && textBlocks.every((text) => (
+ text.includes('') ||
+ text.includes('') ||
+ text.includes('') ||
+ text.includes('')
+ ))
+}
+
function isTaskNotificationContent(content: unknown): boolean {
const textBlocks = extractHistoryTextBlocks(content)
return textBlocks.length > 0 && textBlocks.every((text) => extractTaskNotificationXml(text) !== null)
@@ -3003,6 +3013,9 @@ export function mapHistoryMessagesToUiMessages(
suppressTaskNotificationResponse = true
continue
}
+ if (msg.type === 'user' && isInternalCommandBreadcrumbContent(msg.content)) {
+ continue
+ }
if (msg.type === 'user') {
suppressTaskNotificationResponse = false
} else if (suppressTaskNotificationResponse) {
diff --git a/src/server/__tests__/searchService.sessions.test.ts b/src/server/__tests__/searchService.sessions.test.ts
index b33b155c..a3356bab 100644
--- a/src/server/__tests__/searchService.sessions.test.ts
+++ b/src/server/__tests__/searchService.sessions.test.ts
@@ -150,6 +150,13 @@ describe('SearchService.searchSessions', () => {
it('skips internal command breadcrumb entries', async () => {
await writeSessionFile('proj-a', 'session-6', [
{ type: 'user', message: { role: 'user', content: 'deploy magicword' } },
+ {
+ type: 'user',
+ message: {
+ role: 'user',
+ content: [{ type: 'text', text: 'agent magicword' }],
+ },
+ },
])
const { results } = await service.searchSessions('magicword')
diff --git a/src/server/__tests__/sessions.test.ts b/src/server/__tests__/sessions.test.ts
index af5d4b4e..0aec97cd 100644
--- a/src/server/__tests__/sessions.test.ts
+++ b/src/server/__tests__/sessions.test.ts
@@ -860,6 +860,20 @@ describe('SessionService', () => {
uuid: crypto.randomUUID(),
timestamp: '2026-01-01T00:00:04.000Z',
},
+ {
+ type: 'user',
+ message: {
+ role: 'user',
+ content: [
+ {
+ type: 'text',
+ text: '/agent\nagent\nPlan 222',
+ },
+ ],
+ },
+ uuid: crypto.randomUUID(),
+ timestamp: '2026-01-01T00:00:05.000Z',
+ },
makeAssistantEntry('正常助手消息', crypto.randomUUID()),
])
diff --git a/src/server/__tests__/ws-memory-events.test.ts b/src/server/__tests__/ws-memory-events.test.ts
index dd84bb3e..ed52e52f 100644
--- a/src/server/__tests__/ws-memory-events.test.ts
+++ b/src/server/__tests__/ws-memory-events.test.ts
@@ -27,6 +27,39 @@ describe('WebSocket memory events', () => {
])
})
+ it('does not replay internal slash-command breadcrumbs as user messages', () => {
+ expect(translateCliMessage({
+ type: 'user',
+ isReplay: true,
+ message: {
+ role: 'user',
+ content: [
+ 'agent',
+ '/agent',
+ 'Plan 222',
+ ].join('\n'),
+ },
+ }, 'session-1')).toEqual([])
+
+ expect(translateCliMessage({
+ type: 'user',
+ isReplay: true,
+ message: {
+ role: 'user',
+ content: [
+ {
+ type: 'text',
+ text: [
+ 'agent',
+ '/agent',
+ 'Plan 222',
+ ].join('\n'),
+ },
+ ],
+ },
+ }, 'session-1')).toEqual([])
+ })
+
it('forwards CLI memory_saved system messages to the desktop client', () => {
const messages = translateCliMessage(
{
diff --git a/src/server/services/searchService.ts b/src/server/services/searchService.ts
index d6be3cb5..94725f06 100644
--- a/src/server/services/searchService.ts
+++ b/src/server/services/searchService.ts
@@ -422,12 +422,15 @@ export class SearchService {
}
private isInternalCommandBreadcrumb(content: unknown): boolean {
- if (typeof content !== 'string') return false
+ const textBlocks = this.extractPlainTextBlocks(content)
return (
- content.includes('') ||
- content.includes('') ||
- content.includes('') ||
- content.includes('')
+ textBlocks.length > 0 &&
+ textBlocks.every((text) =>
+ text.includes('') ||
+ text.includes('') ||
+ text.includes('') ||
+ text.includes(''),
+ )
)
}
diff --git a/src/server/services/sessionService.ts b/src/server/services/sessionService.ts
index c4b4c8d1..44799070 100644
--- a/src/server/services/sessionService.ts
+++ b/src/server/services/sessionService.ts
@@ -730,13 +730,15 @@ export class SessionService {
}
private isInternalCommandBreadcrumb(content: unknown): boolean {
- if (typeof content !== 'string') return false
-
+ const textBlocks = this.extractTextBlocks(content)
return (
- content.includes('') ||
- content.includes('') ||
- content.includes('') ||
- content.includes('')
+ textBlocks.length > 0 &&
+ textBlocks.every((text) =>
+ text.includes('') ||
+ text.includes('') ||
+ text.includes('') ||
+ text.includes('')
+ )
)
}
diff --git a/src/server/ws/handler.ts b/src/server/ws/handler.ts
index 5879d942..1cd8ec21 100644
--- a/src/server/ws/handler.ts
+++ b/src/server/ws/handler.ts
@@ -29,7 +29,10 @@ import {
} from '../services/titleService.js'
import { parseSlashCommand } from '../../utils/slashCommandParsing.js'
import {
+ COMMAND_ARGS_TAG,
+ COMMAND_MESSAGE_TAG,
COMMAND_NAME_TAG,
+ LOCAL_COMMAND_CAVEAT_TAG,
LOCAL_COMMAND_STDERR_TAG,
LOCAL_COMMAND_STDOUT_TAG,
} from '../../constants/xml.js'
@@ -2237,9 +2240,34 @@ function hasToolResultBlock(content: unknown): boolean {
(block as { type?: unknown }).type === 'tool_result')
}
+function isInternalCommandBreadcrumb(content: unknown): boolean {
+ const textBlocks = typeof content === 'string'
+ ? [content]
+ : Array.isArray(content)
+ ? content.flatMap((block) => {
+ if (!block || typeof block !== 'object') return []
+ const typedBlock = block as { type?: unknown; text?: unknown }
+ return typedBlock.type === 'text' && typeof typedBlock.text === 'string'
+ ? [typedBlock.text]
+ : []
+ })
+ : []
+
+ return textBlocks.length > 0 && textBlocks.every((text) => {
+ const trimmed = text.trim()
+ return (
+ trimmed.includes(`<${COMMAND_NAME_TAG}>`) ||
+ trimmed.includes(`<${COMMAND_MESSAGE_TAG}>`) ||
+ trimmed.includes(`<${COMMAND_ARGS_TAG}>`) ||
+ trimmed.includes(`<${LOCAL_COMMAND_CAVEAT_TAG}>`)
+ )
+ })
+}
+
function extractReplayUserText(cliMsg: any): string | null {
if (cliMsg?.isReplay !== true) return null
const content = cliMsg.message?.content
+ if (isInternalCommandBreadcrumb(content)) return null
if (isCompactSummaryMessageContent(content)) return null
if (hasToolResultBlock(content)) return null
if (extractLocalCommandOutput(content)) return null