diff --git a/app/renderer/src/main/src/components/yakitUI/YakitMonacoDiffInline/YakitMonacoDiffInline.tsx b/app/renderer/src/main/src/components/yakitUI/YakitMonacoDiffInline/YakitMonacoDiffInline.tsx index 3647a91681..a901f96072 100644 --- a/app/renderer/src/main/src/components/yakitUI/YakitMonacoDiffInline/YakitMonacoDiffInline.tsx +++ b/app/renderer/src/main/src/components/yakitUI/YakitMonacoDiffInline/YakitMonacoDiffInline.tsx @@ -12,6 +12,7 @@ import { useTheme } from '@/hook/useTheme' import { useEditorFontSize } from '@/store/editorFontSize' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import i18n from '@/i18n/i18n' +import { YaklangMonacoSpec } from '@/utils/monacoSpec/yakEditor' import styles from './YakitMonacoDiffInline.module.scss' const tOriginal = i18n.getFixedT(null, ['yakitUi']) @@ -66,6 +67,18 @@ function modEndLineForHunk(hunks: YakitMonacoDiffInlineHunk[], i: number, modMax return Math.min(Math.max(1, modEnd1), modMax) } +const updateEditorValue = (editor: monacoEditor.editor.IStandaloneCodeEditor, nextValue: string) => { + const model = editor.getModel() + if (!model || model.getValue() === nextValue) return + + editor.executeEdits('yakit-monaco-diff-inline', [ + { + range: model.getFullModelRange(), + text: nextValue, + }, + ]) +} + type DiffWidgetHost = { remountWidgets: (after?: () => void) => void } @@ -402,6 +415,19 @@ export const YakitMonacoDiffInline = memo(function YakitMonacoDiffInlineInner(pr const scrollTop = modEditor.getScrollTop() const scrollLeft = modEditor.getScrollLeft() + // 仅 yak 语言走原地更新:避免 diff 重新 setModel 后,绿色修改块反复重绘闪动 + if (language === YaklangMonacoSpec && prevModels?.original.getLanguageId() === language) { + const restoreScroll = () => { + modEditor.setScrollTop(scrollTop) + modEditor.setScrollLeft(scrollLeft) + } + updateEditorValue(diffEditor.getOriginalEditor(), original) + updateEditorValue(modEditor, incoming) + restoreScroll() + widgetHostRef.current?.remountWidgets(restoreScroll) + return + } + const originalModel = monaco.editor.createModel(original, language) const modifiedModel = monaco.editor.createModel(incoming, language) diffEditor.setModel({ original: originalModel, modified: modifiedModel }) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiYaklangCode/AIYaklangCode.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiYaklangCode/AIYaklangCode.tsx index 383add9e22..b043828c95 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiYaklangCode/AIYaklangCode.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiYaklangCode/AIYaklangCode.tsx @@ -13,9 +13,29 @@ import { monaco as monacoApi } from 'react-monaco-editor' import useChatIPCDispatcher from '../../useContext/ChatIPCContent/useDispatcher' import { WebFuzzerAiStore } from '@/pages/ai-agent/store/ChatDataStore' import useGetChatDataStoreKey from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' +import { YakitMonacoDiffInline } from '@/components/yakitUI/YakitMonacoDiffInline/YakitMonacoDiffInline' const CODE_BLOCK_MAX_HEIGHT = 200 +// 将 `*** Begin Patch` 文本块解析为 original / incoming 两份,供 diff 高亮。 +// 约定(unified 风格):`-` 删除行、`+` 新增行、其余(`*** ` 文件头 / `@@ ` 行 / 上下文)两边都放。 +const parsePatchToDiff = (block: string): { original: string; incoming: string } => { + const original: string[] = [] + const incoming: string[] = [] + for (const raw of block.split(/\r?\n/)) { + if (raw === '*** Begin Patch' || raw === '*** End Patch' || raw.startsWith('@@')) continue + const body = raw.slice(1) + if (raw.startsWith('-')) original.push(body) + else if (raw.startsWith('+')) incoming.push(body) + else { + const line = raw.startsWith(' ') ? body : raw + original.push(line) + incoming.push(line) + } + } + return { original: original.join('\n'), incoming: incoming.join('\n') } +} + export const AIYaklangCode: React.FC = React.memo((props) => { const { content: defContent, nodeLabel, modalInfo, contentType, referenceNode } = props @@ -32,6 +52,14 @@ export const AIYaklangCode: React.FC = React.memo((props) => return contentType.split('/')?.[1] || 'plaintext' }, [contentType]) + const diffLanguage = useCreation(() => (type === 'yaklang' ? 'yak' : type), [type]) + // 仅当 content 以 `*** Begin Patch` 开头时,才以 YakitMonacoDiffInline 只读展示 + const isPatch = useCreation(() => defContent.trimStart().startsWith('*** Begin Patch'), [defContent]) + const { original: patchOriginal, incoming: patchIncoming } = useCreation( + () => (isPatch ? parsePatchToDiff(defContent) : { original: '', incoming: '' }), + [isPatch, defContent], + ) + const bindContentHeightEditor = useMemoizedFn((editor: YakitIMonacoEditor) => { const setEditorScrollActive = (active: boolean) => { editor.updateOptions({ @@ -87,6 +115,20 @@ export const AIYaklangCode: React.FC = React.memo((props) => default: // case AIStreamContentType.CODE_YAKLANG: // case AIStreamContentType.CODE_PYTHON: + if (isPatch) { + return ( +
+ {}} + language={diffLanguage} + /> +
+ ) + } return (