mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-26 15:03:34 +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', () => {
|
it('restores persisted image user messages as renderable attachments without exposing image metadata text', () => {
|
||||||
const messages: MessageEntry[] = [
|
const messages: MessageEntry[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2316,6 +2316,16 @@ function extractHistoryTextBlocks(content: unknown): string[] {
|
|||||||
.filter(Boolean)
|
.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 {
|
function isTaskNotificationContent(content: unknown): boolean {
|
||||||
const textBlocks = extractHistoryTextBlocks(content)
|
const textBlocks = extractHistoryTextBlocks(content)
|
||||||
return textBlocks.length > 0 && textBlocks.every((text) => extractTaskNotificationXml(text) !== null)
|
return textBlocks.length > 0 && textBlocks.every((text) => extractTaskNotificationXml(text) !== null)
|
||||||
@ -3003,6 +3013,9 @@ export function mapHistoryMessagesToUiMessages(
|
|||||||
suppressTaskNotificationResponse = true
|
suppressTaskNotificationResponse = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (msg.type === 'user' && isInternalCommandBreadcrumbContent(msg.content)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (msg.type === 'user') {
|
if (msg.type === 'user') {
|
||||||
suppressTaskNotificationResponse = false
|
suppressTaskNotificationResponse = false
|
||||||
} else if (suppressTaskNotificationResponse) {
|
} else if (suppressTaskNotificationResponse) {
|
||||||
|
|||||||
@ -150,6 +150,13 @@ describe('SearchService.searchSessions', () => {
|
|||||||
it('skips internal command breadcrumb entries', async () => {
|
it('skips internal command breadcrumb entries', async () => {
|
||||||
await writeSessionFile('proj-a', 'session-6', [
|
await writeSessionFile('proj-a', 'session-6', [
|
||||||
{ type: 'user', message: { role: 'user', content: '<command-name>deploy</command-name> magicword' } },
|
{ 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')
|
const { results } = await service.searchSessions('magicword')
|
||||||
|
|||||||
@ -860,6 +860,20 @@ describe('SessionService', () => {
|
|||||||
uuid: crypto.randomUUID(),
|
uuid: crypto.randomUUID(),
|
||||||
timestamp: '2026-01-01T00:00:04.000Z',
|
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()),
|
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', () => {
|
it('forwards CLI memory_saved system messages to the desktop client', () => {
|
||||||
const messages = translateCliMessage(
|
const messages = translateCliMessage(
|
||||||
{
|
{
|
||||||
|
|||||||
@ -422,12 +422,15 @@ export class SearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isInternalCommandBreadcrumb(content: unknown): boolean {
|
private isInternalCommandBreadcrumb(content: unknown): boolean {
|
||||||
if (typeof content !== 'string') return false
|
const textBlocks = this.extractPlainTextBlocks(content)
|
||||||
return (
|
return (
|
||||||
content.includes('<command-name>') ||
|
textBlocks.length > 0 &&
|
||||||
content.includes('<command-message>') ||
|
textBlocks.every((text) =>
|
||||||
content.includes('<command-args>') ||
|
text.includes('<command-name>') ||
|
||||||
content.includes('<local-command-caveat>')
|
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 {
|
private isInternalCommandBreadcrumb(content: unknown): boolean {
|
||||||
if (typeof content !== 'string') return false
|
const textBlocks = this.extractTextBlocks(content)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
content.includes('<command-name>') ||
|
textBlocks.length > 0 &&
|
||||||
content.includes('<command-message>') ||
|
textBlocks.every((text) =>
|
||||||
content.includes('<command-args>') ||
|
text.includes('<command-name>') ||
|
||||||
content.includes('<local-command-caveat>')
|
text.includes('<command-message>') ||
|
||||||
|
text.includes('<command-args>') ||
|
||||||
|
text.includes('<local-command-caveat>')
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,10 @@ import {
|
|||||||
} from '../services/titleService.js'
|
} from '../services/titleService.js'
|
||||||
import { parseSlashCommand } from '../../utils/slashCommandParsing.js'
|
import { parseSlashCommand } from '../../utils/slashCommandParsing.js'
|
||||||
import {
|
import {
|
||||||
|
COMMAND_ARGS_TAG,
|
||||||
|
COMMAND_MESSAGE_TAG,
|
||||||
COMMAND_NAME_TAG,
|
COMMAND_NAME_TAG,
|
||||||
|
LOCAL_COMMAND_CAVEAT_TAG,
|
||||||
LOCAL_COMMAND_STDERR_TAG,
|
LOCAL_COMMAND_STDERR_TAG,
|
||||||
LOCAL_COMMAND_STDOUT_TAG,
|
LOCAL_COMMAND_STDOUT_TAG,
|
||||||
} from '../../constants/xml.js'
|
} from '../../constants/xml.js'
|
||||||
@ -2237,9 +2240,34 @@ function hasToolResultBlock(content: unknown): boolean {
|
|||||||
(block as { type?: unknown }).type === 'tool_result')
|
(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 {
|
function extractReplayUserText(cliMsg: any): string | null {
|
||||||
if (cliMsg?.isReplay !== true) return null
|
if (cliMsg?.isReplay !== true) return null
|
||||||
const content = cliMsg.message?.content
|
const content = cliMsg.message?.content
|
||||||
|
if (isInternalCommandBreadcrumb(content)) return null
|
||||||
if (isCompactSummaryMessageContent(content)) return null
|
if (isCompactSummaryMessageContent(content)) return null
|
||||||
if (hasToolResultBlock(content)) return null
|
if (hasToolResultBlock(content)) return null
|
||||||
if (extractLocalCommandOutput(content)) return null
|
if (extractLocalCommandOutput(content)) return null
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user