mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-05-08 08:00:10 +00:00
update apply diff
This commit is contained in:
@@ -17,6 +17,7 @@ import { v4 as uuidv4 } from 'uuid'
|
||||
import { ApplyViewState } from '../../ApplyView'
|
||||
import { APPLY_VIEW_TYPE } from '../../constants'
|
||||
import { useApp } from '../../contexts/AppContext'
|
||||
import { useDiffStrategy } from '../../contexts/DiffStrategyContext'
|
||||
import { useLLM } from '../../contexts/LLMContext'
|
||||
import { useRAG } from '../../contexts/RAGContext'
|
||||
import { useSettings } from '../../contexts/SettingsContext'
|
||||
@@ -93,6 +94,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
const app = useApp()
|
||||
const { settings, setSettings } = useSettings()
|
||||
const { getRAGEngine } = useRAG()
|
||||
const diffStrategy = useDiffStrategy()
|
||||
|
||||
const {
|
||||
createOrUpdateConversation,
|
||||
@@ -104,8 +106,8 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
const { streamResponse, chatModel } = useLLM()
|
||||
|
||||
const promptGenerator = useMemo(() => {
|
||||
return new PromptGenerator(getRAGEngine, app, settings)
|
||||
}, [getRAGEngine, app, settings])
|
||||
return new PromptGenerator(getRAGEngine, app, settings, diffStrategy)
|
||||
}, [getRAGEngine, app, settings, diffStrategy])
|
||||
|
||||
const [inputMessage, setInputMessage] = useState<ChatUserMessage>(() => {
|
||||
const newMessage = getNewInputMessage(app)
|
||||
@@ -382,7 +384,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
if (!applyRes) {
|
||||
throw new Error('Failed to apply edit changes')
|
||||
}
|
||||
// 返回一个Promise,该Promise会在用户做出选择后解析
|
||||
// return a Promise, which will be resolved after user makes a choice
|
||||
return new Promise<{ type: string; applyMsgId: string; applyStatus: ApplyStatus; returnMsg?: ChatUserMessage }>((resolve) => {
|
||||
app.workspace.getLeaf(true).setViewState({
|
||||
type: APPLY_VIEW_TYPE,
|
||||
@@ -419,7 +421,10 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
fileContent,
|
||||
toolArgs.operations
|
||||
)
|
||||
// 返回一个Promise,该Promise会在用户做出选择后解析
|
||||
if (!applyRes) {
|
||||
throw new Error('Failed to search_and_replace')
|
||||
}
|
||||
// return a Promise, which will be resolved after user makes a choice
|
||||
return new Promise<{ type: string; applyMsgId: string; applyStatus: ApplyStatus; returnMsg?: ChatUserMessage }>((resolve) => {
|
||||
app.workspace.getLeaf(true).setViewState({
|
||||
type: APPLY_VIEW_TYPE,
|
||||
@@ -449,6 +454,45 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
} satisfies ApplyViewState,
|
||||
})
|
||||
})
|
||||
} else if (toolArgs.type === 'apply_diff') {
|
||||
const diffResult = await diffStrategy.applyDiff(
|
||||
activeFileContent,
|
||||
toolArgs.diff
|
||||
)
|
||||
if (!diffResult.success) {
|
||||
console.log(diffResult)
|
||||
throw new Error(`Failed to apply_diff`)
|
||||
}
|
||||
// return a Promise, which will be resolved after user makes a choice
|
||||
return new Promise<{ type: string; applyMsgId: string; applyStatus: ApplyStatus; returnMsg?: ChatUserMessage }>((resolve) => {
|
||||
app.workspace.getLeaf(true).setViewState({
|
||||
type: APPLY_VIEW_TYPE,
|
||||
active: true,
|
||||
state: {
|
||||
file: activeFile,
|
||||
originalContent: activeFileContent,
|
||||
newContent: diffResult.content,
|
||||
onClose: (applied: boolean) => {
|
||||
const applyStatus = applied ? ApplyStatus.Applied : ApplyStatus.Rejected
|
||||
const applyEditContent = applied ? 'Changes successfully applied'
|
||||
: 'User rejected changes'
|
||||
resolve({
|
||||
type: 'apply_diff',
|
||||
applyMsgId,
|
||||
applyStatus,
|
||||
returnMsg: {
|
||||
role: 'user',
|
||||
applyStatus: ApplyStatus.Idle,
|
||||
content: null,
|
||||
promptContent: `[apply_diff for '${toolArgs.filepath}'] Result:\n${applyEditContent}\n`,
|
||||
id: uuidv4(),
|
||||
mentionables: [],
|
||||
}
|
||||
});
|
||||
}
|
||||
} satisfies ApplyViewState,
|
||||
})
|
||||
})
|
||||
} else if (toolArgs.type === 'read_file') {
|
||||
const fileContent = activeFile.path === toolArgs.filepath ? activeFileContent : readFileContent(toolArgs.filepath)
|
||||
const formattedContent = `[read_file for '${toolArgs.filepath}'] Result:\n${addLineNumbers(fileContent)}\n`;
|
||||
|
||||
93
src/components/chat-view/MarkdownApplyDiffBlock.tsx
Normal file
93
src/components/chat-view/MarkdownApplyDiffBlock.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Check, Edit, Loader2, X } from 'lucide-react'
|
||||
import { PropsWithChildren, useState } from 'react'
|
||||
|
||||
import { useDarkModeContext } from '../../contexts/DarkModeContext'
|
||||
import { ApplyStatus, ToolArgs } from '../../types/apply'
|
||||
|
||||
import { MemoizedSyntaxHighlighterWrapper } from './SyntaxHighlighterWrapper'
|
||||
|
||||
export default function MarkdownApplyDiffBlock({
|
||||
mode,
|
||||
applyStatus,
|
||||
onApply,
|
||||
path,
|
||||
diff,
|
||||
finish,
|
||||
}: PropsWithChildren<{
|
||||
mode: string
|
||||
applyStatus: ApplyStatus
|
||||
onApply: (args: ToolArgs) => void
|
||||
path: string
|
||||
diff: string
|
||||
finish: boolean
|
||||
}>) {
|
||||
const [applying, setApplying] = useState(false)
|
||||
const { isDarkMode } = useDarkModeContext()
|
||||
|
||||
console.log('MarkdownApplyDiffBlock', { mode, applyStatus, onApply, path, diff, finish })
|
||||
const handleApply = async () => {
|
||||
if (applyStatus !== ApplyStatus.Idle) {
|
||||
return
|
||||
}
|
||||
setApplying(true)
|
||||
onApply({
|
||||
type: "apply_diff",
|
||||
filepath: path,
|
||||
diff,
|
||||
finish,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`infio-chat-code-block ${path ? 'has-filename' : ''} infio-reasoning-block`}>
|
||||
<div className={'infio-chat-code-block-header'}>
|
||||
{path && (
|
||||
<div className={'infio-chat-code-block-header-filename'}>
|
||||
<Edit size={10} className="infio-chat-code-block-header-icon" />
|
||||
{mode}: {path}
|
||||
</div>
|
||||
)}
|
||||
<div className={'infio-chat-code-block-header-button'}>
|
||||
<button
|
||||
onClick={handleApply}
|
||||
style={{ color: '#008000' }}
|
||||
disabled={applyStatus !== ApplyStatus.Idle || applying || !finish}
|
||||
>
|
||||
{
|
||||
!finish ? (
|
||||
<>
|
||||
<Loader2 className="spinner" size={14} /> Loading...
|
||||
</>
|
||||
) : applyStatus === ApplyStatus.Idle ? (
|
||||
applying ? (
|
||||
<>
|
||||
<Loader2 className="spinner" size={14} /> Applying...
|
||||
</>
|
||||
) : (
|
||||
'Apply'
|
||||
)
|
||||
) : applyStatus === ApplyStatus.Applied ? (
|
||||
<>
|
||||
<Check size={14} /> Success
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<X size={14} /> Failed
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="infio-reasoning-content-wrapper">
|
||||
<MemoizedSyntaxHighlighterWrapper
|
||||
isDarkMode={isDarkMode}
|
||||
language="diff"
|
||||
hasFilename={!!path}
|
||||
wrapLines={true}
|
||||
>
|
||||
{diff}
|
||||
</MemoizedSyntaxHighlighterWrapper>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +1,29 @@
|
||||
import { Check, Loader2, Replace, X } from 'lucide-react'
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
import { useApp } from '../../contexts/AppContext'
|
||||
import { ApplyStatus, SearchAndReplaceToolArgs } from '../../types/apply'
|
||||
import { openMarkdownFile } from '../../utils/obsidian'
|
||||
import { MemoizedSyntaxHighlighterWrapper } from './SyntaxHighlighterWrapper'
|
||||
import { useDarkModeContext } from '../../contexts/DarkModeContext'
|
||||
|
||||
export default function MarkdownSearchAndReplace({
|
||||
applyStatus,
|
||||
onApply,
|
||||
path,
|
||||
content,
|
||||
operations,
|
||||
finish
|
||||
}: {
|
||||
applyStatus: ApplyStatus
|
||||
onApply: (args: SearchAndReplaceToolArgs) => void
|
||||
path: string,
|
||||
content: string,
|
||||
operations: SearchAndReplaceToolArgs['operations'],
|
||||
finish: boolean
|
||||
}) {
|
||||
const app = useApp()
|
||||
const { isDarkMode } = useDarkModeContext()
|
||||
|
||||
const [applying, setApplying] = React.useState(false)
|
||||
|
||||
@@ -51,9 +56,13 @@ export default function MarkdownSearchAndReplace({
|
||||
<div className={'infio-chat-code-block-header-button'}>
|
||||
<button
|
||||
onClick={handleApply}
|
||||
disabled={applyStatus !== ApplyStatus.Idle || applying}
|
||||
disabled={applyStatus !== ApplyStatus.Idle || applying || !finish}
|
||||
>
|
||||
{applyStatus === ApplyStatus.Idle ? (
|
||||
{!finish ? (
|
||||
<>
|
||||
<Loader2 className="spinner" size={14} />
|
||||
</>
|
||||
) : applyStatus === ApplyStatus.Idle ? (
|
||||
applying ? (
|
||||
<>
|
||||
<Loader2 className="spinner" size={14} /> Applying...
|
||||
@@ -73,6 +82,14 @@ export default function MarkdownSearchAndReplace({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<MemoizedSyntaxHighlighterWrapper
|
||||
isDarkMode={isDarkMode}
|
||||
language="markdown"
|
||||
hasFilename={!!path}
|
||||
wrapLines={true}
|
||||
>
|
||||
{content}
|
||||
</MemoizedSyntaxHighlighterWrapper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
parseMsgBlocks,
|
||||
} from '../../utils/parse-infio-block'
|
||||
|
||||
import MarkdownApplyDiffBlock from './MarkdownApplyDiffBlock'
|
||||
import MarkdownEditFileBlock from './MarkdownEditFileBlock'
|
||||
import MarkdownFetchUrlsContentBlock from './MarkdownFetchUrlsContentBlock'
|
||||
import MarkdownListFilesBlock from './MarkdownListFilesBlock'
|
||||
@@ -18,6 +19,7 @@ import MarkdownSearchWebBlock from './MarkdownSearchWebBlock'
|
||||
import MarkdownSemanticSearchFilesBlock from './MarkdownSemanticSearchFilesBlock'
|
||||
import MarkdownSwitchModeBlock from './MarkdownSwitchModeBlock'
|
||||
import MarkdownWithIcons from './MarkdownWithIcon'
|
||||
|
||||
function ReactMarkdown({
|
||||
applyStatus,
|
||||
onApply,
|
||||
@@ -27,6 +29,7 @@ function ReactMarkdown({
|
||||
onApply: (toolArgs: ToolArgs) => void
|
||||
children: string
|
||||
}) {
|
||||
|
||||
const blocks: ParsedMsgBlock[] = useMemo(
|
||||
() => parseMsgBlocks(children),
|
||||
[children],
|
||||
@@ -73,6 +76,7 @@ function ReactMarkdown({
|
||||
applyStatus={applyStatus}
|
||||
onApply={onApply}
|
||||
path={block.path}
|
||||
content={block.content}
|
||||
operations={block.operations.map(op => ({
|
||||
search: op.search,
|
||||
replace: op.replace,
|
||||
@@ -84,6 +88,16 @@ function ReactMarkdown({
|
||||
}))}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'apply_diff' ? (
|
||||
<MarkdownApplyDiffBlock
|
||||
key={"apply-diff-" + index}
|
||||
applyStatus={applyStatus}
|
||||
mode={block.type}
|
||||
onApply={onApply}
|
||||
path={block.path}
|
||||
diff={block.diff}
|
||||
finish={block.finish}
|
||||
/>
|
||||
) : block.type === 'read_file' ? (
|
||||
<MarkdownReadFileBlock
|
||||
key={"read-file-" + index}
|
||||
|
||||
Reference in New Issue
Block a user