From 5886a84c4225b29da22175c8e5922910d5cffb8b Mon Sep 17 00:00:00 2001 From: Nang Date: Tue, 22 Oct 2024 21:43:26 -0400 Subject: [PATCH] Aider part 2 (#32) * Added command retries * barechatfilecontext * Added Reset Session * Added progress * Added working * Added cleanup --------- Co-authored-by: nang-dev Co-authored-by: Himanshu --- core/config/default.ts | 2 + core/config/load.ts | 6 +- core/config/types.ts | 4 +- .../providers/RelativeFileContextProvider.ts | 69 +++++++++++ core/context/providers/index.ts | 5 + core/index.d.ts | 4 +- core/llm/llms/Aider.ts | 116 ++++++++++-------- .../components/mainInput/ContinueInputBox.tsx | 22 +++- gui/src/components/mainInput/TipTapEditor.tsx | 4 +- gui/src/components/mainInput/resolveInput.ts | 53 ++++---- gui/src/pages/gui.tsx | 116 +++++++++--------- gui/src/util/bareChatMode.ts | 13 +- 12 files changed, 265 insertions(+), 149 deletions(-) create mode 100644 core/context/providers/RelativeFileContextProvider.ts diff --git a/core/config/default.ts b/core/config/default.ts index dd2a095c2c..ad29ea63f2 100644 --- a/core/config/default.ts +++ b/core/config/default.ts @@ -34,6 +34,7 @@ export const FREE_TRIAL_MODELS: ModelDescription[] = [ ]; export const defaultContextProvidersVsCode: ContextProviderWithParams[] = [ + { name: "file", params: {} }, { name: "directory", params: {} }, { name: "code", params: {} }, { name: "docs", params: {} }, @@ -42,6 +43,7 @@ export const defaultContextProvidersVsCode: ContextProviderWithParams[] = [ { name: "problems", params: {} }, { name: "folder", params: {} }, { name: "codebase", params: {} }, + { name: "relativefilecontext", params: {} }, ]; export const defaultContextProvidersJetBrains: ContextProviderWithParams[] = [ diff --git a/core/config/load.ts b/core/config/load.ts index 54a0278f17..7697cd697d 100644 --- a/core/config/load.ts +++ b/core/config/load.ts @@ -372,9 +372,9 @@ async function intermediateToFinalConfig( // These context providers are always included, regardless of what, if anything, // the user has configured in config.json - const DEFAULT_CONTEXT_PROVIDERS = [ - new FileContextProvider({}), - new CodebaseContextProvider({}), + const DEFAULT_CONTEXT_PROVIDERS : any[] = [ + // new FileContextProvider({}), + // new CodebaseContextProvider({}), ]; const DEFAULT_CONTEXT_PROVIDERS_TITLES = DEFAULT_CONTEXT_PROVIDERS.map( diff --git a/core/config/types.ts b/core/config/types.ts index 89ea6495fe..041ca3a66b 100644 --- a/core/config/types.ts +++ b/core/config/types.ts @@ -453,6 +453,7 @@ declare global { | "DraftIssueStep"; type ContextProviderName = + | "file" | "diff" | "github" | "terminal" @@ -471,7 +472,8 @@ declare global { | "code" | "docs" | "gitlab-mr" - | "os"; + | "os" + | "relativefilecontext"; type TemplateType = | "llama2" diff --git a/core/context/providers/RelativeFileContextProvider.ts b/core/context/providers/RelativeFileContextProvider.ts new file mode 100644 index 0000000000..5ed4d2183b --- /dev/null +++ b/core/context/providers/RelativeFileContextProvider.ts @@ -0,0 +1,69 @@ +import { + ContextItem, + ContextProviderDescription, + ContextProviderExtras, + ContextSubmenuItem, + LoadSubmenuItemsArgs, +} from "../../index.js"; +import { walkDir } from "../../indexing/walkDir.js"; +import { + getBasename, + getUniqueFilePath, + groupByLastNPathParts, +} from "../../util/index.js"; +import { BaseContextProvider } from "../index.js"; + +const MAX_SUBMENU_ITEMS = 10_000; + +class RelativeFileContextProvider extends BaseContextProvider { + static description: ContextProviderDescription = { + title: "relativefilecontext", + displayTitle: "Files", + description: "Add file to context.", + type: "submenu", + }; + + async getContextItems( + query: string, + extras: ContextProviderExtras, + ): Promise { + const workspaceDirs = await extras.ide.getWorkspaceDirs(); + const relativePath = this.normalizeRelativePath(query, workspaceDirs[0]); + return [ + { + name: getBasename(query), + description: relativePath, + content: relativePath, + }, + ]; + } + + async loadSubmenuItems( + args: LoadSubmenuItemsArgs, + ): Promise { + const workspaceDirs = await args.ide.getWorkspaceDirs(); + const results = await Promise.all( + workspaceDirs.map((dir) => { + return walkDir(dir, args.ide); + }), + ); + const files = results.flat().slice(-MAX_SUBMENU_ITEMS); + const fileGroups = groupByLastNPathParts(files, 2); + + return files.map((file) => { + const relativePath = this.normalizeRelativePath(file, workspaceDirs[0]); + return { + id: file, + title: getBasename(file), + description: relativePath, + }; + }); + } + + private normalizeRelativePath(path: string, workspaceDir: string): string { + const relativePath = path.replace(workspaceDir, "").replace(/^[\/\\]/, ""); + return relativePath.replace(/\\/g, "/"); + } +} + +export default RelativeFileContextProvider; diff --git a/core/context/providers/index.ts b/core/context/providers/index.ts index 462fe0db49..72213ba1e4 100644 --- a/core/context/providers/index.ts +++ b/core/context/providers/index.ts @@ -22,6 +22,9 @@ import ProblemsContextProvider from "./ProblemsContextProvider.js"; import SearchContextProvider from "./SearchContextProvider.js"; import TerminalContextProvider from "./TerminalContextProvider.js"; import URLContextProvider from "./URLContextProvider.js"; +import RelativeFileContextProvider from "./RelativeFileContextProvider.js"; +import FileContextProvider from "./FileContextProvider.js"; + /** * Note: We are currently omitting the following providers due to bugs: @@ -31,6 +34,7 @@ import URLContextProvider from "./URLContextProvider.js"; * See this issue for details: https://github.com/continuedev/continue/issues/1365 */ const Providers: (typeof BaseContextProvider)[] = [ + FileContextProvider, DiffContextProvider, FileTreeContextProvider, GitHubIssuesContextProvider, @@ -52,6 +56,7 @@ const Providers: (typeof BaseContextProvider)[] = [ CurrentFileContextProvider, URLContextProvider, ContinueProxyContextProvider, + RelativeFileContextProvider ]; export function contextProviderClassFromName( diff --git a/core/index.d.ts b/core/index.d.ts index 7a368a6bff..0ef3edd003 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -550,6 +550,7 @@ type StepName = | "DraftIssueStep"; type ContextProviderName = + | "file" | "diff" | "github" | "terminal" @@ -569,7 +570,8 @@ type ContextProviderName = | "docs" | "gitlab-mr" | "os" - | "currentFile"; + | "currentFile" + | "relativefilecontext"; type TemplateType = | "llama2" diff --git a/core/llm/llms/Aider.ts b/core/llm/llms/Aider.ts index 84903e52ed..1e9af2bb39 100644 --- a/core/llm/llms/Aider.ts +++ b/core/llm/llms/Aider.ts @@ -158,56 +158,74 @@ public async aiderResetSession(model: string, apiKey: string | undefined): Promi let command: string[]; - switch (model) { - case "claude-3-5-sonnet-20240620": - console.log("claude model chosen"); - command = ["aider --model claude-3-5-sonnet-20240620"]; - break; - case "gpt-4o": - command = ["aider --model gpt-4o"]; - break; - case "pearai_model": - default: - await this.credentials.checkAndUpdateCredentials(); - const accessToken = this.credentials.getAccessToken(); - if (!accessToken) { - let message = "PearAI token invalid. Please try logging in or contact PearAI support." - vscode.window - .showErrorMessage( - message, - 'Login To PearAI', - 'Show Logs', - ) - .then((selection: any) => { - if (selection === 'Login To PearAI') { - // Redirect to auth login URL - vscode.env.openExternal( - vscode.Uri.parse( - 'https://trypear.ai/signin?callback=pearai://pearai.pearai/auth', - ), - ); - } else if (selection === 'Show Logs') { - vscode.commands.executeCommand( - 'workbench.action.toggleDevTools', - ); + const aiderFlags = "--no-pretty --yes-always --no-auto-commits"; + const aiderCommands = [ + `python -m aider ${aiderFlags}`, + `python3 -m aider ${aiderFlags}`, + `aider ${aiderFlags}` + ]; + let commandFound = false; + + for (const aiderCommand of aiderCommands) { + try { + await execSync(`${aiderCommand} --version`, { stdio: 'ignore' }); + commandFound = true; + + switch (model) { + case model.includes("claude") && model: + command = [`${aiderCommand} --model ${model}`]; + break; + case "gpt-4o": + command = [`${aiderCommand} --model gpt-4o`]; + break; + case "pearai_model": + default: + await this.credentials.checkAndUpdateCredentials(); + const accessToken = this.credentials.getAccessToken(); + if (!accessToken) { + let message = "PearAI token invalid. Please try logging in or contact PearAI support." + vscode.window + .showErrorMessage( + message, + 'Login To PearAI', + 'Show Logs', + ) + .then((selection: any) => { + if (selection === 'Login To PearAI') { + // Redirect to auth login URL + vscode.env.openExternal( + vscode.Uri.parse( + 'https://trypear.ai/signin?callback=pearai://pearai.pearai/auth', + ), + ); + } else if (selection === 'Show Logs') { + vscode.commands.executeCommand( + 'workbench.action.toggleDevTools', + ); + } + }); + throw new Error("User not logged in to PearAI."); } - }); - throw new Error("User not logged in to PearAI."); + command = [ + aiderCommand, + "--openai-api-key", + accessToken, + "--openai-api-base", + `${SERVER_URL}/integrations/aider`, + ]; + break; } - command = [ - "aider", - "--openai-api-key", - accessToken, - "--openai-api-base", - `${SERVER_URL}/integrations/aider`, - ]; - break; + break; // Exit the loop if a working command is found + } catch (error) { + console.log(`Command ${aiderCommand} not found or errored. Trying next...`); + } + } + + if (!commandFound) { + throw new Error("Aider command not found. Please ensure it's installed correctly."); } - // disable pretty printing - command.push("--no-pretty"); - command.push("--yes-always"); - command.push("--no-auto-commits"); + const userPath = this.getUserPath(); const userShell = this.getUserShell(); @@ -254,6 +272,8 @@ public async aiderResetSession(model: string, apiKey: string | undefined): Promi env: { ...process.env, PATH: userPath, + PYTHONIOENCODING: "utf-8", + AIDER_SIMPLE_OUTPUT: "1", }, windowsHide: true, }); @@ -343,7 +363,8 @@ public async aiderResetSession(model: string, apiKey: string | undefined): Promi this.aiderProcess.stdin && !this.aiderProcess.killed ) { - this.aiderProcess.stdin.write(`${message}\n`); + const formattedMessage = message.replace(/\n+/g, " "); + this.aiderProcess.stdin.write(`${formattedMessage}\n`); } else { console.error("Aider process is not running"); } @@ -404,7 +425,6 @@ public async aiderResetSession(model: string, apiKey: string | undefined): Promi options: CompletionOptions, ): AsyncGenerator { console.log("Inside Aider _streamChat"); - const lastMessage = messages[messages.length - 1].content.toString(); this.sendToAiderChat(lastMessage); diff --git a/gui/src/components/mainInput/ContinueInputBox.tsx b/gui/src/components/mainInput/ContinueInputBox.tsx index 7f05708659..d517f6b63c 100644 --- a/gui/src/components/mainInput/ContinueInputBox.tsx +++ b/gui/src/components/mainInput/ContinueInputBox.tsx @@ -11,7 +11,7 @@ import ContextItemsPeek from "./ContextItemsPeek"; import TipTapEditor from "./TipTapEditor"; import { useMemo } from "react"; import { defaultModelSelector } from "../../redux/selectors/modelSelectors"; -import { isBareChatMode } from '../../util/bareChatMode'; +import { isBareChatMode } from "../../util/bareChatMode"; const gradient = keyframes` 0% { @@ -67,11 +67,19 @@ function ContinueInputBox(props: ContinueInputBoxProps) { const active = useSelector((store: RootState) => store.state.active); const availableSlashCommands = useSelector(selectSlashCommands); - const availableContextProviders = useSelector( + let availableContextProviders = useSelector( (store: RootState) => store.state.config.contextProviders, ); - const bareChatMode = isBareChatMode(); - + const bareChatMode = isBareChatMode() + const filteredContextProviders = useMemo(() => { + return bareChatMode + ? availableContextProviders.filter( + (provider) => provider.title === "relativefilecontext", + ) + : availableContextProviders.filter( + (provider) => provider.title !== "relativefilecontext", + ); + }, [bareChatMode, availableContextProviders]); useWebviewListener( "newSessionWithPrompt", @@ -108,8 +116,10 @@ function ContinueInputBox(props: ContinueInputBoxProps) { editorState={props.editorState} onEnter={props.onEnter} isMainInput={props.isMainInput} - availableContextProviders={bareChatMode ? undefined : availableContextProviders} - availableSlashCommands={bareChatMode ? undefined : availableSlashCommands} + availableContextProviders={filteredContextProviders} + availableSlashCommands={ + bareChatMode ? undefined : availableSlashCommands + } > diff --git a/gui/src/components/mainInput/TipTapEditor.tsx b/gui/src/components/mainInput/TipTapEditor.tsx index 310d713d6f..2b2fcd5ab0 100644 --- a/gui/src/components/mainInput/TipTapEditor.tsx +++ b/gui/src/components/mainInput/TipTapEditor.tsx @@ -211,9 +211,7 @@ function TipTapEditor(props: TipTapEditorProps) { const defaultModel = useSelector(defaultModelSelector); const bareChatMode = isBareChatMode(); const getSubmenuContextItemsRef = useUpdatingRef(getSubmenuContextItems); - const availableContextProvidersRef = useUpdatingRef( - props.availableContextProviders, - ); + const availableContextProvidersRef = useUpdatingRef(props.availableContextProviders) const historyLengthRef = useUpdatingRef(historyLength); const availableSlashCommandsRef = useUpdatingRef( diff --git a/gui/src/components/mainInput/resolveInput.ts b/gui/src/components/mainInput/resolveInput.ts index 7b2c207823..1fb3b99c8f 100644 --- a/gui/src/components/mainInput/resolveInput.ts +++ b/gui/src/components/mainInput/resolveInput.ts @@ -116,23 +116,27 @@ async function resolveEditorContent( } } - const previousDirectoryItems = (store.getState() as any).state.directoryItems; - - // use directory structure - const directoryItems = await ideMessenger.request("context/getContextItems", { - name: "directory", - query: "", - fullInput: stripImages(parts), - selectedCode, - }); - - // if (previousDirectoryItems !== directoryItems[0].content) { - // store.dispatch(setDirectoryItems(directoryItems[0].content)); - // contextItems.push(...directoryItems); - // for (const codebaseItem of directoryItems) { - // contextItemsText += codebaseItem.content + "\n\n"; - // } - // } + const defaultModelTitle = (store.getState() as any).state.defaultModelTitle; + const isBareChatMode = defaultModelTitle?.toLowerCase().includes("aider"); + + if (!isBareChatMode) { + const previousDirectoryItems = (store.getState() as any).state.directoryItems; + // use directory structure + const directoryItems = await ideMessenger.request("context/getContextItems", { + name: "directory", + query: "", + fullInput: stripImages(parts), + selectedCode, + }); + + if (previousDirectoryItems !== directoryItems[0].content) { + store.dispatch(setDirectoryItems(directoryItems[0].content)); + contextItems.push(...directoryItems); + for (const codebaseItem of directoryItems) { + contextItemsText += codebaseItem.content + "\n\n"; + } + } + } // cmd+enter to use codebase if (modifiers.useCodebase) { @@ -165,6 +169,7 @@ async function resolveEditorContent( } } + return [contextItems, selectedCode, parts]; } @@ -181,6 +186,8 @@ function findLastIndex( } function resolveParagraph(p: JSONContent): [string, MentionAttrs[], string] { + const defaultModelTitle = (store.getState() as any).state.defaultModelTitle; + const isBareChatMode = defaultModelTitle?.toLowerCase().includes("aider"); let text = ""; const contextItems = []; let slashCommand = undefined; @@ -188,10 +195,14 @@ function resolveParagraph(p: JSONContent): [string, MentionAttrs[], string] { if (child.type === "text") { text += text === "" ? child.text.trimStart() : child.text; } else if (child.type === "mention") { - text += - typeof child.attrs.renderInlineAs === "string" - ? child.attrs.renderInlineAs - : child.attrs.label; + // console.dir("MENTION") + // console.dir(child) + if (!isBareChatMode) { + text += + typeof child.attrs.renderInlineAs === "string" + ? child.attrs.renderInlineAs + : child.attrs.label; + } contextItems.push(child.attrs); } else if (child.type === "slashcommand") { if (typeof slashCommand === "undefined") { diff --git a/gui/src/pages/gui.tsx b/gui/src/pages/gui.tsx index e51292dc59..76a40bfe2e 100644 --- a/gui/src/pages/gui.tsx +++ b/gui/src/pages/gui.tsx @@ -17,7 +17,7 @@ import { } from "react"; import { ErrorBoundary } from "react-error-boundary"; import { useDispatch, useSelector } from "react-redux"; -import { useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import styled from "styled-components"; import { Button, @@ -197,6 +197,7 @@ function GUI() { const posthog = usePostHog(); const dispatch = useDispatch(); const navigate = useNavigate(); + const location = useLocation(); const ideMessenger = useContext(IdeMessengerContext); const sessionState = useSelector((state: RootState) => state.state); @@ -226,6 +227,7 @@ function GUI() { ); const bareChatMode = isBareChatMode(); + const aiderMode = location?.pathname === "/aiderMode" const onCloseTutorialCard = () => { posthog.capture("closedTutorialCard"); @@ -443,7 +445,7 @@ function GUI() { <>
- {defaultModel?.title?.toLowerCase().includes("aider") && ( + {aiderMode && (

PearAI Creator- Beta

{" "} @@ -579,67 +581,61 @@ function GUI() { isMainInput={true} hidden={active} > - {active ? ( - <> -
-
- - ) : state.history.length > 0 ? ( -
- { - saveSession(); - if (defaultModel?.title?.toLowerCase().includes("aider")) { - ideMessenger.post("aiderResetSession", undefined) - } - }} - className="mr-auto" - > - New Session - {!bareChatMode && ` (${getMetaKeyLabel()} ${isJetBrains() ? "J" : "L"})`} - {" "} - {!bareChatMode && !!showAiderHint && } - {/* { - navigate("/inventory"); - }} - className="mr-auto" - > - Inventory - {" "} */} -
- ) : ( - <> - {getLastSessionId() ? ( -
+ {active ? ( + <> +
+
+ + ) : state.history.length > 0 ? ( +
+ {aiderMode ? ( { - loadLastSession(); + onClick={() => { + saveSession(); + ideMessenger.post("aiderResetSession", undefined) }} - className="mr-auto flex items-center gap-2" + className="mr-auto" > - - Last Session + Restart Session -
- ) : null} - {/* { - navigate("/inventory"); - }} - className="mr-auto" - > - PearAI Inventory - {" "} */} - - {!!showTutorialCard && ( -
- -
- )} - {!bareChatMode && !!showAiderHint && } - - )} + ) : ( + <> + { + saveSession(); + }} + className="mr-auto" + > + New Session + {!bareChatMode && ` (${getMetaKeyLabel()} ${isJetBrains() ? "J" : "L"})`} + + {!bareChatMode && !!showAiderHint && } + + )} +
+) : ( + <> + {!aiderMode && getLastSessionId() ? ( +
+ { + loadLastSession(); + }} + className="mr-auto flex items-center gap-2" + > + + Last Session + +
+ ) : null} + {!!showTutorialCard && ( +
+ +
+ )} + {!bareChatMode && !aiderMode && !!showAiderHint && } + +)}
defaultModel?.title?.toLowerCase().includes("aider"), - [defaultModel] - ); +const BARE_CHAT_PATHS = ['/aiderMode']; + +export function isBareChatMode() { + const location = useLocation(); + return BARE_CHAT_PATHS.includes(location?.pathname); } +