mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
fix(desktop): hide slash command breadcrumbs on restore (#833)
Tested: bun test src/server/__tests__/ws-memory-events.test.ts Tested: bun test src/server/__tests__/sessions.test.ts -t "command breadcrumb" Tested: bun test src/server/__tests__/searchService.sessions.test.ts Tested: cd desktop && bun run test -- --run src/stores/chatStore.test.ts Tested: bun run check:desktop Tested: bun run check:server Scope-risk: narrow Confidence: high
This commit is contained in:
parent
8d26c2c154
commit
0a7db60394
@ -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: [
|
||||
'<command-message>agent</command-message>',
|
||||
'<command-name>/agent</command-name>',
|
||||
'<command-args>Plan 222</command-args>',
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
id: 'agent-command-array',
|
||||
type: 'user',
|
||||
timestamp: '2026-06-15T03:32:14.000Z',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: [
|
||||
'<command-message>agent</command-message>',
|
||||
'<command-name>/agent</command-name>',
|
||||
'<command-args>Plan 333</command-args>',
|
||||
].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[] = [
|
||||
{
|
||||
|
||||
@ -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('<command-name>') ||
|
||||
text.includes('<command-message>') ||
|
||||
text.includes('<command-args>') ||
|
||||
text.includes('<local-command-caveat>')
|
||||
))
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@ -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: '<command-name>deploy</command-name> magicword' } },
|
||||
{
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: '<command-message>agent</command-message> magicword' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const { results } = await service.searchSessions('magicword')
|
||||
|
||||
@ -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: '<command-name>/agent</command-name>\n<command-message>agent</command-message>\n<command-args>Plan 222</command-args>',
|
||||
},
|
||||
],
|
||||
},
|
||||
uuid: crypto.randomUUID(),
|
||||
timestamp: '2026-01-01T00:00:05.000Z',
|
||||
},
|
||||
makeAssistantEntry('正常助手消息', crypto.randomUUID()),
|
||||
])
|
||||
|
||||
|
||||
@ -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: [
|
||||
'<command-message>agent</command-message>',
|
||||
'<command-name>/agent</command-name>',
|
||||
'<command-args>Plan 222</command-args>',
|
||||
].join('\n'),
|
||||
},
|
||||
}, 'session-1')).toEqual([])
|
||||
|
||||
expect(translateCliMessage({
|
||||
type: 'user',
|
||||
isReplay: true,
|
||||
message: {
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: [
|
||||
'<command-message>agent</command-message>',
|
||||
'<command-name>/agent</command-name>',
|
||||
'<command-args>Plan 222</command-args>',
|
||||
].join('\n'),
|
||||
},
|
||||
],
|
||||
},
|
||||
}, 'session-1')).toEqual([])
|
||||
})
|
||||
|
||||
it('forwards CLI memory_saved system messages to the desktop client', () => {
|
||||
const messages = translateCliMessage(
|
||||
{
|
||||
|
||||
@ -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('<command-name>') ||
|
||||
content.includes('<command-message>') ||
|
||||
content.includes('<command-args>') ||
|
||||
content.includes('<local-command-caveat>')
|
||||
textBlocks.length > 0 &&
|
||||
textBlocks.every((text) =>
|
||||
text.includes('<command-name>') ||
|
||||
text.includes('<command-message>') ||
|
||||
text.includes('<command-args>') ||
|
||||
text.includes('<local-command-caveat>'),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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('<command-name>') ||
|
||||
content.includes('<command-message>') ||
|
||||
content.includes('<command-args>') ||
|
||||
content.includes('<local-command-caveat>')
|
||||
textBlocks.length > 0 &&
|
||||
textBlocks.every((text) =>
|
||||
text.includes('<command-name>') ||
|
||||
text.includes('<command-message>') ||
|
||||
text.includes('<command-args>') ||
|
||||
text.includes('<local-command-caveat>')
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user