mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
fix(desktop): restore diff reference cards #1004
This commit is contained in:
parent
465fbafb3d
commit
59a7bad2f9
@ -693,6 +693,8 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
|||||||
isDirectory: reference.isDirectory,
|
isDirectory: reference.isDirectory,
|
||||||
lineStart: reference.lineStart,
|
lineStart: reference.lineStart,
|
||||||
lineEnd: reference.lineEnd,
|
lineEnd: reference.lineEnd,
|
||||||
|
diffSide: reference.diffSide,
|
||||||
|
hunkId: reference.hunkId,
|
||||||
note: reference.note,
|
note: reference.note,
|
||||||
quote: reference.quote,
|
quote: reference.quote,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@ -1565,6 +1565,46 @@ describe('chatStore history mapping', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('restores persisted workspace diff comments without exposing the model prompt', () => {
|
||||||
|
const modelPrompt = [
|
||||||
|
'@"/repo/homepage/src/App.vue" Referenced workspace context:',
|
||||||
|
'@"homepage/src/App.vue:new:L94-L105":',
|
||||||
|
'Comment: 这块儿我们不能再修改一下',
|
||||||
|
'```vue',
|
||||||
|
'<section id="hero" class="pt-32 pb-20 px-6">',
|
||||||
|
' <h1>{{ name }}</h1>',
|
||||||
|
'</section>',
|
||||||
|
'```',
|
||||||
|
].join('\n')
|
||||||
|
const mapped = mapHistoryMessagesToUiMessages([
|
||||||
|
{
|
||||||
|
id: 'workspace-comment-user-1',
|
||||||
|
type: 'user',
|
||||||
|
timestamp: '2026-07-14T00:00:00.000Z',
|
||||||
|
content: [{ type: 'text', text: modelPrompt }],
|
||||||
|
} as MessageEntry,
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(mapped).toMatchObject([
|
||||||
|
{
|
||||||
|
id: 'workspace-comment-user-1',
|
||||||
|
type: 'user_text',
|
||||||
|
content: '',
|
||||||
|
modelContent: modelPrompt,
|
||||||
|
attachments: [{
|
||||||
|
type: 'file',
|
||||||
|
name: 'App.vue',
|
||||||
|
path: 'homepage/src/App.vue',
|
||||||
|
lineStart: 94,
|
||||||
|
lineEnd: 105,
|
||||||
|
diffSide: 'new',
|
||||||
|
note: '这块儿我们不能再修改一下',
|
||||||
|
quote: '<section id="hero" class="pt-32 pb-20 px-6">\n <h1>{{ name }}</h1>\n</section>',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps workspace reference chips visible while sending CLI attachment paths', () => {
|
it('keeps workspace reference chips visible while sending CLI attachment paths', () => {
|
||||||
useChatStore.setState({
|
useChatStore.setState({
|
||||||
sessions: {
|
sessions: {
|
||||||
@ -1610,6 +1650,8 @@ describe('chatStore history mapping', () => {
|
|||||||
path: 'src/App.tsx',
|
path: 'src/App.tsx',
|
||||||
lineStart: 4,
|
lineStart: 4,
|
||||||
lineEnd: 4,
|
lineEnd: 4,
|
||||||
|
diffSide: 'new',
|
||||||
|
hunkId: 'hunk-1',
|
||||||
note: 'tighten this',
|
note: 'tighten this',
|
||||||
quote: 'const value = 1',
|
quote: 'const value = 1',
|
||||||
}],
|
}],
|
||||||
@ -1627,6 +1669,8 @@ describe('chatStore history mapping', () => {
|
|||||||
path: 'src/App.tsx',
|
path: 'src/App.tsx',
|
||||||
lineStart: 4,
|
lineStart: 4,
|
||||||
lineEnd: 4,
|
lineEnd: 4,
|
||||||
|
diffSide: 'new',
|
||||||
|
hunkId: 'hunk-1',
|
||||||
note: 'tighten this',
|
note: 'tighten this',
|
||||||
quote: 'const value = 1',
|
quote: 'const value = 1',
|
||||||
}],
|
}],
|
||||||
@ -5096,6 +5140,44 @@ describe('chatStore history mapping', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('restores workspace diff comment styling from a replayed model prompt', () => {
|
||||||
|
const modelPrompt = [
|
||||||
|
'@"/repo/src/App.vue" Referenced workspace context:',
|
||||||
|
'@"src/App.vue:new:L94-L105":',
|
||||||
|
'Comment: 调整这里',
|
||||||
|
'```vue',
|
||||||
|
'<section id="hero">',
|
||||||
|
'```',
|
||||||
|
].join('\n')
|
||||||
|
useChatStore.setState({
|
||||||
|
sessions: {
|
||||||
|
[TEST_SESSION_ID]: makeSession({ chatState: 'thinking' }),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
||||||
|
type: 'user_message_replay',
|
||||||
|
content: modelPrompt,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages).toMatchObject([
|
||||||
|
{
|
||||||
|
type: 'user_text',
|
||||||
|
content: '',
|
||||||
|
modelContent: modelPrompt,
|
||||||
|
attachments: [{
|
||||||
|
type: 'file',
|
||||||
|
path: 'src/App.vue',
|
||||||
|
diffSide: 'new',
|
||||||
|
lineStart: 94,
|
||||||
|
lineEnd: 105,
|
||||||
|
note: '调整这里',
|
||||||
|
quote: '<section id="hero">',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it('does not leak an image-bearing prompt when the replay appends [Image source] metadata (Windows path)', () => {
|
it('does not leak an image-bearing prompt when the replay appends [Image source] metadata (Windows path)', () => {
|
||||||
// The optimistic message (e.g. a visual-selection annotation card) stores the
|
// The optimistic message (e.g. a visual-selection annotation card) stores the
|
||||||
// prompt body in modelContent with a hidden display. The CLI replay carries
|
// prompt body in modelContent with a hidden display. The CLI replay carries
|
||||||
|
|||||||
@ -1155,6 +1155,8 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
|||||||
mimeType: a.mimeType,
|
mimeType: a.mimeType,
|
||||||
lineStart: a.lineStart,
|
lineStart: a.lineStart,
|
||||||
lineEnd: a.lineEnd,
|
lineEnd: a.lineEnd,
|
||||||
|
diffSide: a.diffSide,
|
||||||
|
hunkId: a.hunkId,
|
||||||
note: a.note,
|
note: a.note,
|
||||||
quote: a.quote,
|
quote: a.quote,
|
||||||
}))
|
}))
|
||||||
@ -3552,6 +3554,133 @@ function extractLeadingFileReferences(text: string): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WorkspaceReferenceHistoryDisplay = {
|
||||||
|
content: string
|
||||||
|
attachments: UIAttachment[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWorkspaceReferenceLocation(location: string): {
|
||||||
|
path: string
|
||||||
|
lineStart?: number
|
||||||
|
lineEnd?: number
|
||||||
|
diffSide?: 'old' | 'new'
|
||||||
|
} {
|
||||||
|
const match = location.match(/^(.*?)(?::(old|new))?:L(\d+)(?:-L(\d+))?$/)
|
||||||
|
if (!match?.[1] || !match[3]) return { path: location }
|
||||||
|
|
||||||
|
const lineStart = Number(match[3])
|
||||||
|
const lineEnd = Number(match[4] ?? match[3])
|
||||||
|
return {
|
||||||
|
path: match[1],
|
||||||
|
lineStart,
|
||||||
|
lineEnd,
|
||||||
|
...(match[2] === 'old' || match[2] === 'new' ? { diffSide: match[2] } : {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWorkspaceReferenceHistoryPrompt(text: string): WorkspaceReferenceHistoryDisplay | null {
|
||||||
|
const lines = text.replace(/\r\n?/g, '\n').split('\n')
|
||||||
|
if (lines[0]?.trim() !== 'Referenced workspace context:') return null
|
||||||
|
|
||||||
|
const attachments: UIAttachment[] = []
|
||||||
|
let index = 1
|
||||||
|
const isLocationHeader = (line: string) => /^@".+":$/.test(line.trim())
|
||||||
|
|
||||||
|
while (index < lines.length) {
|
||||||
|
const header = lines[index]?.trim() ?? ''
|
||||||
|
const locationMatch = header.match(/^@"(.+)":$/)
|
||||||
|
if (!locationMatch?.[1]) break
|
||||||
|
|
||||||
|
const location = parseWorkspaceReferenceLocation(locationMatch[1])
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
let note: string | undefined
|
||||||
|
if (lines[index]?.trimStart().startsWith('Comment:')) {
|
||||||
|
const noteLines = [lines[index]!.trimStart().slice('Comment:'.length).trimStart()]
|
||||||
|
index += 1
|
||||||
|
while (
|
||||||
|
index < lines.length &&
|
||||||
|
lines[index]!.trim() !== '' &&
|
||||||
|
!/^`{3,}/.test(lines[index]!.trim()) &&
|
||||||
|
!isLocationHeader(lines[index]!)
|
||||||
|
) {
|
||||||
|
noteLines.push(lines[index]!)
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
note = noteLines.join('\n').trim() || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
let quote: string | undefined
|
||||||
|
const fenceMatch = lines[index]?.trim().match(/^(`{3,})[^`]*$/)
|
||||||
|
if (fenceMatch?.[1]) {
|
||||||
|
const fence = fenceMatch[1]
|
||||||
|
index += 1
|
||||||
|
const quoteLines: string[] = []
|
||||||
|
while (index < lines.length && lines[index]?.trim() !== fence) {
|
||||||
|
quoteLines.push(lines[index]!)
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
if (index >= lines.length) return null
|
||||||
|
index += 1
|
||||||
|
quote = quoteLines.join('\n').trim() || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments.push({
|
||||||
|
type: 'file',
|
||||||
|
name: getReferenceName(location.path),
|
||||||
|
path: location.path,
|
||||||
|
...(location.lineStart ? { lineStart: location.lineStart } : {}),
|
||||||
|
...(location.lineEnd ? { lineEnd: location.lineEnd } : {}),
|
||||||
|
...(location.diffSide ? { diffSide: location.diffSide } : {}),
|
||||||
|
...(note ? { note } : {}),
|
||||||
|
...(quote ? { quote } : {}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachments.length === 0) return null
|
||||||
|
while (lines[index]?.trim() === '') index += 1
|
||||||
|
return {
|
||||||
|
content: lines.slice(index).join('\n').trim(),
|
||||||
|
attachments,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathsReferToSameFile(left: string | undefined, right: string | undefined): boolean {
|
||||||
|
if (!left || !right) return false
|
||||||
|
const normalizedLeft = left.replace(/\\/g, '/').replace(/^\.\//, '')
|
||||||
|
const normalizedRight = right.replace(/\\/g, '/').replace(/^\.\//, '')
|
||||||
|
return (
|
||||||
|
normalizedLeft === normalizedRight ||
|
||||||
|
normalizedLeft.endsWith(`/${normalizedRight}`) ||
|
||||||
|
normalizedRight.endsWith(`/${normalizedLeft}`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractRestoredUserDisplay(text: string): {
|
||||||
|
content: string
|
||||||
|
attachments?: UIAttachment[]
|
||||||
|
modelContent?: string
|
||||||
|
} {
|
||||||
|
const leading = extractLeadingFileReferences(text)
|
||||||
|
const workspace = parseWorkspaceReferenceHistoryPrompt(leading.content)
|
||||||
|
if (!workspace) return leading
|
||||||
|
|
||||||
|
const unmatchedLeading = [...(leading.attachments ?? [])]
|
||||||
|
for (const attachment of workspace.attachments) {
|
||||||
|
const matchingIndex = unmatchedLeading.findIndex((candidate) =>
|
||||||
|
pathsReferToSameFile(candidate.path, attachment.path),
|
||||||
|
)
|
||||||
|
if (matchingIndex >= 0) unmatchedLeading.splice(matchingIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachments = [...unmatchedLeading, ...workspace.attachments]
|
||||||
|
return {
|
||||||
|
content: workspace.content,
|
||||||
|
attachments: attachments.length > 0 ? attachments : undefined,
|
||||||
|
modelContent: text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function appendReplayedUserMessage(
|
export function appendReplayedUserMessage(
|
||||||
messages: UIMessage[],
|
messages: UIMessage[],
|
||||||
content: string,
|
content: string,
|
||||||
@ -3562,9 +3691,9 @@ export function appendReplayedUserMessage(
|
|||||||
// mapping) so the dedupe below can match the already-rendered message instead
|
// mapping) so the dedupe below can match the already-rendered message instead
|
||||||
// of appending the raw prompt — paths and all — as a duplicate bubble.
|
// of appending the raw prompt — paths and all — as a duplicate bubble.
|
||||||
const sanitized = stripGeneratedImageMetadataLines(content) || content.trim()
|
const sanitized = stripGeneratedImageMetadataLines(content) || content.trim()
|
||||||
const parsed = extractLeadingFileReferences(sanitized)
|
const parsed = extractRestoredUserDisplay(sanitized)
|
||||||
const displayContent = parsed.content.trim() || sanitized
|
const displayContent = parsed.content.trim()
|
||||||
if (!displayContent) return messages
|
if (!displayContent && !parsed.attachments?.length) return messages
|
||||||
|
|
||||||
const modelContent = parsed.modelContent ?? sanitized
|
const modelContent = parsed.modelContent ?? sanitized
|
||||||
const currentTurnUserIndex = findCurrentTurnUserMessageIndex(messages, modelContent)
|
const currentTurnUserIndex = findCurrentTurnUserMessageIndex(messages, modelContent)
|
||||||
@ -3629,6 +3758,8 @@ function mapQueuedDisplayAttachments(attachments?: AttachmentRef[]): UIAttachmen
|
|||||||
isDirectory: attachment.isDirectory,
|
isDirectory: attachment.isDirectory,
|
||||||
lineStart: attachment.lineStart,
|
lineStart: attachment.lineStart,
|
||||||
lineEnd: attachment.lineEnd,
|
lineEnd: attachment.lineEnd,
|
||||||
|
diffSide: attachment.diffSide,
|
||||||
|
hunkId: attachment.hunkId,
|
||||||
note: attachment.note,
|
note: attachment.note,
|
||||||
quote: attachment.quote,
|
quote: attachment.quote,
|
||||||
}))
|
}))
|
||||||
@ -3865,7 +3996,7 @@ export function mapHistoryMessagesToUiMessages(
|
|||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const parsed = extractLeadingFileReferences(msg.content)
|
const parsed = extractRestoredUserDisplay(msg.content)
|
||||||
uiMessages.push({
|
uiMessages.push({
|
||||||
id: msg.id || nextId(),
|
id: msg.id || nextId(),
|
||||||
type: 'user_text',
|
type: 'user_text',
|
||||||
@ -3943,7 +4074,7 @@ export function mapHistoryMessagesToUiMessages(
|
|||||||
if (visualSelectionDisplay) {
|
if (visualSelectionDisplay) {
|
||||||
applyVisualSelectionHistoryDisplay(attachments, visualSelectionDisplay)
|
applyVisualSelectionHistoryDisplay(attachments, visualSelectionDisplay)
|
||||||
}
|
}
|
||||||
const parsed = extractLeadingFileReferences(visibleText)
|
const parsed = extractRestoredUserDisplay(visibleText)
|
||||||
const userContent = visualSelectionDisplay ? '' : parsed.content
|
const userContent = visualSelectionDisplay ? '' : parsed.content
|
||||||
const modelContent = visualSelectionDisplay || modelText !== visibleText ? modelText : parsed.modelContent
|
const modelContent = visualSelectionDisplay || modelText !== visibleText ? modelText : parsed.modelContent
|
||||||
const allAttachments = [...(parsed.attachments ?? []), ...attachments]
|
const allAttachments = [...(parsed.attachments ?? []), ...attachments]
|
||||||
|
|||||||
@ -38,6 +38,8 @@ export type AttachmentRef = {
|
|||||||
isDirectory?: boolean
|
isDirectory?: boolean
|
||||||
lineStart?: number
|
lineStart?: number
|
||||||
lineEnd?: number
|
lineEnd?: number
|
||||||
|
diffSide?: 'old' | 'new'
|
||||||
|
hunkId?: string
|
||||||
note?: string
|
note?: string
|
||||||
quote?: string
|
quote?: string
|
||||||
}
|
}
|
||||||
@ -69,6 +71,8 @@ export type UIAttachment = {
|
|||||||
isDirectory?: boolean
|
isDirectory?: boolean
|
||||||
lineStart?: number
|
lineStart?: number
|
||||||
lineEnd?: number
|
lineEnd?: number
|
||||||
|
diffSide?: 'old' | 'new'
|
||||||
|
hunkId?: string
|
||||||
note?: string
|
note?: string
|
||||||
quote?: string
|
quote?: string
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user