From 589938f8fde791450d1b08f04a490ab3bb9749cd Mon Sep 17 00:00:00 2001 From: Marc McIntosh Date: Tue, 18 Feb 2025 14:16:56 +0100 Subject: [PATCH 1/4] chore(chaT): sync with changes to alpha, include new patches. --- refact-agent/gui/package-lock.json | 21 +- refact-agent/gui/package.json | 6 +- refact-agent/gui/src/__fixtures__/index.ts | 1 + refact-agent/gui/src/__fixtures__/msw.ts | 13 + .../gui/src/__tests__/PinMessages.test.tsx | 120 -------- refact-agent/gui/src/app/middleware.ts | 45 --- refact-agent/gui/src/app/store.ts | 3 - .../gui/src/components/Callout/Callout.tsx | 4 + .../ChatContent/ChatContent.stories.tsx | 34 +++ .../components/ChatContent/ToolsContent.tsx | 12 + .../gui/src/components/ChatForm/ChatForm.tsx | 19 +- .../src/components/ChatForm/useInputValue.ts | 12 +- .../src/components/ComboBox/ComboBox.test.tsx | 16 +- .../gui/src/components/ComboBox/ComboBox.tsx | 28 +- .../gui/src/components/Markdown/CodeBlock.tsx | 3 +- .../gui/src/components/Markdown/Markdown.tsx | 160 +---------- .../gui/src/components/TextArea/TextArea.tsx | 6 +- .../src/components/Tools/Texdoc.module.css | 47 ++++ .../gui/src/components/Tools/Textdoc.tsx | 263 ++++++++++++++++++ .../gui/src/components/Tools/types.ts | 181 ++++++++++++ refact-agent/gui/src/events/index.ts | 11 +- .../gui/src/features/Chat/activeFile.ts | 1 + refact-agent/gui/src/hooks/index.ts | 1 - .../gui/src/hooks/useEventBusForApp.ts | 14 +- .../gui/src/hooks/useEventBusForIDE.ts | 52 ++-- refact-agent/gui/src/hooks/useLinksFromLsp.ts | 27 +- refact-agent/gui/src/hooks/usePatchActions.ts | 189 ------------- .../gui/src/services/refact/consts.ts | 1 + refact-agent/gui/src/services/refact/diffs.ts | 231 +-------------- refact-agent/gui/src/services/refact/index.ts | 1 - refact-agent/gui/src/services/refact/tools.ts | 61 +++- .../gui/src/utils/extractFilePathFromPin.ts | 11 - refact-agent/gui/src/utils/index.ts | 2 +- refact-agent/gui/src/utils/isDetailMessage.ts | 27 ++ 34 files changed, 775 insertions(+), 848 deletions(-) delete mode 100644 refact-agent/gui/src/__tests__/PinMessages.test.tsx create mode 100644 refact-agent/gui/src/components/Tools/Texdoc.module.css create mode 100644 refact-agent/gui/src/components/Tools/Textdoc.tsx create mode 100644 refact-agent/gui/src/components/Tools/types.ts delete mode 100644 refact-agent/gui/src/hooks/usePatchActions.ts delete mode 100644 refact-agent/gui/src/utils/extractFilePathFromPin.ts create mode 100644 refact-agent/gui/src/utils/isDetailMessage.ts diff --git a/refact-agent/gui/package-lock.json b/refact-agent/gui/package-lock.json index 08564292a..cd7e6de0d 100644 --- a/refact-agent/gui/package-lock.json +++ b/refact-agent/gui/package-lock.json @@ -1,12 +1,12 @@ { "name": "refact-chat-js", - "version": "2.0.8-alpha.1", + "version": "2.0.8-alpha.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "refact-chat-js", - "version": "2.0.8-alpha.1", + "version": "2.0.8-alpha.2", "license": "BSD-3-Clause", "dependencies": { "@reduxjs/toolkit": "^2.2.7", @@ -34,6 +34,7 @@ "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.1", "@types/debug": "^4.1.12", + "@types/diff": "^7.0.1", "@types/js-cookie": "^3.0.6", "@types/lodash.groupby": "^4.6.9", "@types/lodash.isequal": "^4.5.8", @@ -48,6 +49,7 @@ "@vitest/coverage-v8": "^1.1.0", "@vitest/ui": "^1.1.0", "classnames": "^2.3.2", + "diff": "^7.0.0", "echarts": "^5.4.3", "echarts-for-react": "^3.0.2", "eslint": "^8.55.0", @@ -9272,6 +9274,12 @@ "integrity": "sha512-Rf3/lB9WkDfIL9eEKaSYKc+1L/rNVYBjThk22JTqQw0YozXarX8YljFAz+HCoC6h4B4KwCMsBPZHaFezwT4BNA==", "dev": true }, + "node_modules/@types/diff": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.1.tgz", + "integrity": "sha512-R/BHQFripuhW6XPXy05hIvXJQdQ4540KnTvEFHSLjXfHYM41liOLKgIJEyYYiQe796xpaMHfe4Uj/p7Uvng2vA==", + "dev": true + }, "node_modules/@types/doctrine": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.3.tgz", @@ -12390,6 +12398,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", diff --git a/refact-agent/gui/package.json b/refact-agent/gui/package.json index e085ced22..9ef5f0c82 100644 --- a/refact-agent/gui/package.json +++ b/refact-agent/gui/package.json @@ -1,6 +1,6 @@ { "name": "refact-chat-js", - "version": "2.0.8-alpha.1", + "version": "2.0.8-alpha.2", "type": "module", "license": "BSD-3-Clause", "files": [ @@ -38,7 +38,7 @@ "coverage": "vitest run --coverage", "format:check": "prettier . --check", "format": "prettier . --write", - "prepare": "", + "prepare": "husky install", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "types": "tsc --noEmit", @@ -72,6 +72,7 @@ "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.1", "@types/debug": "^4.1.12", + "@types/diff": "^7.0.1", "@types/js-cookie": "^3.0.6", "@types/lodash.groupby": "^4.6.9", "@types/lodash.isequal": "^4.5.8", @@ -86,6 +87,7 @@ "@vitest/coverage-v8": "^1.1.0", "@vitest/ui": "^1.1.0", "classnames": "^2.3.2", + "diff": "^7.0.0", "echarts": "^5.4.3", "echarts-for-react": "^3.0.2", "eslint": "^8.55.0", diff --git a/refact-agent/gui/src/__fixtures__/index.ts b/refact-agent/gui/src/__fixtures__/index.ts index 4110e97d9..b50208fc5 100644 --- a/refact-agent/gui/src/__fixtures__/index.ts +++ b/refact-agent/gui/src/__fixtures__/index.ts @@ -7,3 +7,4 @@ export * from "./integrations"; export * from "./survey_questions"; export * from "./chat_links_response"; export * from "./chat_config_thread"; +export * from "./chat_textdoc"; diff --git a/refact-agent/gui/src/__fixtures__/msw.ts b/refact-agent/gui/src/__fixtures__/msw.ts index 3064aa071..aedc22052 100644 --- a/refact-agent/gui/src/__fixtures__/msw.ts +++ b/refact-agent/gui/src/__fixtures__/msw.ts @@ -14,6 +14,7 @@ import { STUB_SUB_RESPONSE, STUB_SUB_RESPONSE_WITH_STATUS } from "./knowledge"; import { GoodPollingResponse } from "../services/smallcloud"; import type { LinksForChatResponse } from "../services/refact/links"; import { CompressTrajectoryResponse } from "../services/refact/knowledge"; +import { ToolConfirmationResponse } from "../services/refact"; export const goodPing: HttpHandler = http.get( "http://127.0.0.1:8001/v1/ping", @@ -287,3 +288,15 @@ export const telemetryNetwork = http.post( }); }, ); + +export const ToolConfirmation = http.post( + "http://127.0.0.1:8001/v1/tools-check-if-confirmation-needed", + () => { + const response: ToolConfirmationResponse = { + pause: false, + pause_reasons: [], + }; + + return HttpResponse.json(response); + }, +); diff --git a/refact-agent/gui/src/__tests__/PinMessages.test.tsx b/refact-agent/gui/src/__tests__/PinMessages.test.tsx deleted file mode 100644 index af9096426..000000000 --- a/refact-agent/gui/src/__tests__/PinMessages.test.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { render } from "../utils/test-utils"; -import { describe, expect, test } from "vitest"; -import { - server, - goodPrompts, - goodCaps, - noTools, - noCommandPreview, - noCompletions, - goodUser, - goodPing, - chatLinks, - telemetryChat, - telemetryNetwork, -} from "../utils/mockServer"; -import { InnerApp } from "../features/App"; - -describe("Pin messages", () => { - server.use( - goodPing, - goodCaps, - goodPrompts, - noTools, - noCommandPreview, - noCompletions, - goodUser, - chatLinks, - telemetryChat, - telemetryNetwork, - ); - - test("it should replace 📍PARTIAL_EDIT 000 /Users/refact/code/refact-lsp/src/ast/ast_db.rs", () => { - const app = render(, { - preloadedState: { - pages: [{ name: "chat" }], - config: { - apiKey: "test", - lspPort: 8001, - themeProps: {}, - host: "vscode", - addressURL: "Refact", - }, - chat: { - prevent_send: false, - streaming: false, - error: null, - waiting_for_response: false, - max_new_tokens: 4096, - cache: {}, - system_prompt: {}, - tool_use: "explore", - send_immediately: false, - thread: { - id: "8ab48978-3ad3-4387-944c-fedd15bdbc0d", - new_chat_suggested: { - wasSuggested: false, - }, - messages: [ - { - role: "context_file", - content: [ - { - file_name: "refact-lsp/src/ast/ast_db.rs", - file_content: "big file\n", - line1: 1, - line2: 999, - }, - ], - }, - { - role: "user", - content: - "\n```rust\n doc_add(\n ast_index.clone(),\n &library_file_path.to_string(),\n &library_text,\n &mut errstats,\n )\n\n```\n\nmake it one line\n", - checkpoints: [], - }, - { - role: "assistant", - content: - "To make the `doc_add` function call a single line, I will remove the line breaks and format it accordingly. Let's proceed with the change.", - tool_calls: [ - { - index: 0, - id: "call_5YBv9DPZETyjfpmsfwU0BbVm", - type: "function", - function: { - name: "search", - arguments: '{"query":"doc_add(","scope":"workspace"}', - }, - }, - ], - }, - { - role: "tool", - content: { - tool_call_id: "call_5YBv9DPZETyjfpmsfwU0BbVm", - content: - "Search produced no results, adjust the query or try a different scope.", - }, - }, - { - role: "assistant", - content: - "It seems that the `doc_add` function is not defined within the current workspace. However, I can still proceed with making the requested change to the function call in the test module. Let's update the code accordingly.\n📍PARTIAL_EDIT 000 /Users/kot/code/refact-lsp/src/ast/ast_db.rs\n```rust\n doc_add(ast_index.clone(), &library_file_path.to_string(), &library_text, &mut errstats)\n```", - }, - ], - title: - "```rust\n doc_add(\n ast_index.clone(),\n &library_file_path.to_string(),\n &library_text,\n &mut errstats,\n )\n\n```\n\nmake it one line\n", - model: "gpt-4o", - tool_use: "explore", - read: true, - createdAt: "2024-10-20T05:03:15.550Z", - updatedAt: "2024-10-20T05:03:15.550Z", - }, - }, - }, - }); - - expect(() => app.getAllByText(/📍/g)).throws(); - }); -}); diff --git a/refact-agent/gui/src/app/middleware.ts b/refact-agent/gui/src/app/middleware.ts index 82e28d197..9f3faf2d5 100644 --- a/refact-agent/gui/src/app/middleware.ts +++ b/refact-agent/gui/src/app/middleware.ts @@ -20,7 +20,6 @@ import { promptsApi } from "../services/refact/prompts"; import { toolsApi } from "../services/refact/tools"; import { commandsApi, isDetailMessage } from "../services/refact/commands"; import { pathApi } from "../services/refact/path"; -import { diffApi } from "../services/refact/diffs"; import { pingApi } from "../services/refact/ping"; import { clearError, @@ -66,7 +65,6 @@ startListening({ // promptsApi.util.resetApiState(), toolsApi.util.resetApiState(), commandsApi.util.resetApiState(), - diffApi.util.resetApiState(), resetAttachedImagesSlice(), resetConfirmationInteractedState(), ].forEach((api) => listenerApi.dispatch(api)); @@ -314,19 +312,6 @@ startListening({ ) { listenerApi.dispatch(setError(action.payload)); } - - if (diffApi.endpoints.applyAllPatchesInMessages.matchRejected(action)) { - const errorStatus = action.payload?.status; - const isAuthError = errorStatus === 401; - const message = isAuthError - ? AUTH_ERROR_MESSAGE - : isDetailMessage(action.payload?.data) - ? action.payload.data.detail - : `Failed to apply diffs: ${action.payload?.status}`; - - listenerApi.dispatch(setError(message)); - listenerApi.dispatch(setIsAuthError(isAuthError)); - } }, }); @@ -404,8 +389,6 @@ startListening({ matcher: isAnyOf( chatAskQuestionThunk.rejected.match, chatAskQuestionThunk.fulfilled.match, - diffApi.endpoints.patchSingleFileFromTicket.matchFulfilled, - diffApi.endpoints.patchSingleFileFromTicket.matchRejected, // give files api pathApi.endpoints.getFullPath.matchFulfilled, pathApi.endpoints.getFullPath.matchRejected, @@ -458,34 +441,6 @@ startListening({ void listenerApi.dispatch(thunk); } - if (diffApi.endpoints.patchSingleFileFromTicket.matchFulfilled(action)) { - const success = !action.payload.results.every( - (result) => result.already_applied, - ); - const thunk = telemetryApi.endpoints.sendTelemetryChatEvent.initiate({ - scope: "handleShow", - success: success, - error_message: success - ? "" - : "Already applied, no significant changes generated.", - }); - - void listenerApi.dispatch(thunk); - } - - if ( - diffApi.endpoints.patchSingleFileFromTicket.matchRejected(action) && - !action.meta.condition - ) { - const thunk = telemetryApi.endpoints.sendTelemetryChatEvent.initiate({ - scope: "handleShow", - success: false, - error_message: action.error.message ?? JSON.stringify(action.error), - }); - - void listenerApi.dispatch(thunk); - } - if (pathApi.endpoints.getFullPath.matchFulfilled(action)) { const thunk = telemetryApi.endpoints.sendTelemetryNetEvent.initiate({ url: FULL_PATH_URL, diff --git a/refact-agent/gui/src/app/store.ts b/refact-agent/gui/src/app/store.ts index 656ffc04c..49b48ee80 100644 --- a/refact-agent/gui/src/app/store.ts +++ b/refact-agent/gui/src/app/store.ts @@ -16,7 +16,6 @@ import { promptsApi, toolsApi, commandsApi, - diffApi, pathApi, pingApi, integrationsApi, @@ -94,7 +93,6 @@ const rootReducer = combineSlices( [promptsApi.reducerPath]: promptsApi.reducer, [toolsApi.reducerPath]: toolsApi.reducer, [commandsApi.reducerPath]: commandsApi.reducer, - [diffApi.reducerPath]: diffApi.reducer, [smallCloudApi.reducerPath]: smallCloudApi.reducer, [pathApi.reducerPath]: pathApi.reducer, [pingApi.reducerPath]: pingApi.reducer, @@ -180,7 +178,6 @@ export function setUpStore(preloadedState?: Partial) { promptsApi.middleware, toolsApi.middleware, commandsApi.middleware, - diffApi.middleware, smallCloudApi.middleware, pathApi.middleware, linksApi.middleware, diff --git a/refact-agent/gui/src/components/Callout/Callout.tsx b/refact-agent/gui/src/components/Callout/Callout.tsx index 55e60da78..0020dd24e 100644 --- a/refact-agent/gui/src/components/Callout/Callout.tsx +++ b/refact-agent/gui/src/components/Callout/Callout.tsx @@ -32,6 +32,7 @@ export const Callout: React.FC = ({ type = "info", timeout = null, onClick = () => void 0, + preventRetry = false, ...props }) => { const [isOpened, setIsOpened] = useState(false); @@ -47,6 +48,8 @@ export const Callout: React.FC = ({ }, []); const handleRetryClick = () => { + // TBD: why was this added, it won't close on click :/? + if (preventRetry) return; setIsOpened(false); const timeoutId = setTimeout(() => { onClick(); @@ -84,6 +87,7 @@ export const Callout: React.FC = ({ ); }; +// TODO: Authcall out should not be generic ErrorCallout export const ErrorCallout: React.FC> = ({ timeout = null, onClick, diff --git a/refact-agent/gui/src/components/ChatContent/ChatContent.stories.tsx b/refact-agent/gui/src/components/ChatContent/ChatContent.stories.tsx index ccc67468e..7418f0e21 100644 --- a/refact-agent/gui/src/components/ChatContent/ChatContent.stories.tsx +++ b/refact-agent/gui/src/components/ChatContent/ChatContent.stories.tsx @@ -17,9 +17,21 @@ import { CHAT_WITH_MULTI_MODAL, CHAT_CONFIG_THREAD, STUB_LINKS_FOR_CHAT_RESPONSE, + CHAT_WITH_TEXTDOC, } from "../../__fixtures__"; import { http, HttpResponse } from "msw"; import { CHAT_LINKS_URL } from "../../services/refact/consts"; +import { + goodCaps, + goodPing, + goodPrompts, + goodUser, + makeKnowledgeFromChat, + noCommandPreview, + noCompletions, + noTools, + ToolConfirmation, +} from "../../__fixtures__/msw"; const MockedStore: React.FC<{ messages?: ChatMessages; @@ -145,3 +157,25 @@ export const IntegrationChat: Story = { }, }, }; + +export const TextDoc: Story = { + args: { + thread: CHAT_WITH_TEXTDOC, + }, + parameters: { + msw: { + handlers: [ + goodCaps, + goodPing, + goodPrompts, + goodUser, + // noChatLinks, + noTools, + makeKnowledgeFromChat, + ToolConfirmation, + noCompletions, + noCommandPreview, + ], + }, + }, +}; diff --git a/refact-agent/gui/src/components/ChatContent/ToolsContent.tsx b/refact-agent/gui/src/components/ChatContent/ToolsContent.tsx index 5bece56e4..27ebda6bb 100644 --- a/refact-agent/gui/src/components/ChatContent/ToolsContent.tsx +++ b/refact-agent/gui/src/components/ChatContent/ToolsContent.tsx @@ -33,6 +33,8 @@ import { DialogImage } from "../DialogImage"; import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons"; import { RootState } from "../../app/store"; import { selectFeatures } from "../../features/Config/configSlice"; +import { isRawTextDocToolCall } from "../Tools/types"; +import { TextDocTool } from "../Tools/Textdoc"; type ResultProps = { children: string; @@ -247,6 +249,16 @@ function processToolCalls( return processToolCalls(tail, toolResults, features, [...processed, elem]); } + if (isRawTextDocToolCall(head)) { + const elem = ( + + ); + return processToolCalls(tail, toolResults, features, [...processed, elem]); + } + if (result && isMultiModalToolResult(result)) { const restInTail = takeWhile(tail, (toolCall) => { const nextResult = toolResults.find( diff --git a/refact-agent/gui/src/components/ChatForm/ChatForm.tsx b/refact-agent/gui/src/components/ChatForm/ChatForm.tsx index e07005929..56c3c3bd2 100644 --- a/refact-agent/gui/src/components/ChatForm/ChatForm.tsx +++ b/refact-agent/gui/src/components/ChatForm/ChatForm.tsx @@ -17,6 +17,7 @@ import { useAgentUsage, useCapsForToolUse, USAGE_LIMIT_EXHAUSTED_MESSAGE, + useSendChatRequest, } from "../../hooks"; import { ErrorCallout, Callout } from "../Callout"; import { ComboBox } from "../ComboBox"; @@ -25,7 +26,7 @@ import { ChatControls } from "./ChatControls"; import { addCheckboxValuesToInput } from "./utils"; import { useCommandCompletionAndPreviewFiles } from "./useCommandCompletionAndPreviewFiles"; import { useAppSelector, useAppDispatch } from "../../hooks"; -import { getErrorMessage, clearError } from "../../features/Errors/errorsSlice"; +import { clearError, getErrorMessage } from "../../features/Errors/errorsSlice"; import { useTourRefs } from "../../features/Tour"; import { useCheckboxes } from "./useCheckBoxes"; import { useInputValue } from "./useInputValue"; @@ -41,6 +42,7 @@ import { AttachFileButton, FileList } from "../Dropzone"; import { useAttachedImages } from "../../hooks/useAttachedImages"; import { enableSend, + selectChatError, selectChatId, selectIsStreaming, selectIsWaiting, @@ -72,12 +74,14 @@ export const ChatForm: React.FC = ({ const { isMultimodalitySupportedForCurrentModel } = useCapsForToolUse(); const config = useConfig(); const toolUse = useAppSelector(selectToolUse); - const error = useAppSelector(getErrorMessage); + const globalError = useAppSelector(getErrorMessage); + const chatError = useAppSelector(selectChatError); const information = useAppSelector(getInformationMessage); const pauseReasonsWithPause = useAppSelector(getPauseReasonsWithPauseStatus); const [helpInfo, setHelpInfo] = React.useState(null); const { disableInput } = useAgentUsage(); const isOnline = useIsOnline(); + const { retry } = useSendChatRequest(); const chatId = useAppSelector(selectChatId); const threadToolUse = useAppSelector(selectThreadToolUse); @@ -88,7 +92,12 @@ export const ChatForm: React.FC = ({ return threadToolUse === "agent" && toolUse === "agent"; }, [toolUse, threadToolUse]); - const onClearError = useCallback(() => dispatch(clearError()), [dispatch]); + const onClearError = useCallback(() => { + if (messages.length > 0 && chatError) { + retry(messages); + } + dispatch(clearError()); + }, [dispatch, retry, messages, chatError]); const caps = useCapsForToolUse(); @@ -266,10 +275,10 @@ export const ChatForm: React.FC = ({ setIsSendImmediately, ]); - if (error) { + if (globalError) { return ( - {error} + {globalError} ); } diff --git a/refact-agent/gui/src/components/ChatForm/useInputValue.ts b/refact-agent/gui/src/components/ChatForm/useInputValue.ts index ba2d670ad..e55ad46bb 100644 --- a/refact-agent/gui/src/components/ChatForm/useInputValue.ts +++ b/refact-agent/gui/src/components/ChatForm/useInputValue.ts @@ -34,7 +34,10 @@ export function useInputValue( (event: MessageEvent) => { if (addInputValue.match(event.data) || setInputValue.match(event.data)) { const { payload } = event.data; - debugRefact(`[DEBUG]: receiving event setInputValue/addInputValue`); + debugRefact( + `[DEBUG]: receiving event setInputValue/addInputValue with payload:`, + payload, + ); setUpIfNotReady(); if (payload.messages) { @@ -49,14 +52,19 @@ export function useInputValue( if (addInputValue.match(event.data)) { const { payload } = event.data; + debugRefact(`[DEBUG]: addInputValue triggered with:`, payload); const { send_immediately, value } = payload; - setValue((prev) => prev + value); + setValue((prev) => { + debugRefact(`[DEBUG]: Previous value: "${prev}", Adding: "${value}"`); + return prev + value; + }); setIsSendImmediately(send_immediately); return; } if (setInputValue.match(event.data)) { const { payload } = event.data; + debugRefact(`[DEBUG]: setInputValue triggered with:`, payload); const { send_immediately, value } = payload; uncheckCheckboxes(); setValue(value ?? ""); diff --git a/refact-agent/gui/src/components/ComboBox/ComboBox.test.tsx b/refact-agent/gui/src/components/ComboBox/ComboBox.test.tsx index 3f0edac50..2969885b4 100644 --- a/refact-agent/gui/src/components/ComboBox/ComboBox.test.tsx +++ b/refact-agent/gui/src/components/ComboBox/ComboBox.test.tsx @@ -377,6 +377,8 @@ describe("ComboBox", () => { await user.keyboard("{z}"); expect(textarea.textContent).toEqual("@file "); + await pause(100); // required, because of cancelling on frequent paste + await user.keyboard("{z}{/Meta}{/Shift}"); expect(textarea.textContent).toEqual("@file /foo "); }); @@ -387,23 +389,29 @@ describe("ComboBox", () => { await user.type(textarea, "@"); await user.keyboard("{Enter}"); - await pause(50); + await pause(150); await user.keyboard("{Enter}"); expect(textarea.textContent).toEqual("@file /foo "); + await user.keyboard("{Control>}{z}"); expect(textarea.textContent).toEqual("@file "); + await user.keyboard("{z}"); expect(textarea.textContent).toEqual("@"); + await user.keyboard("{z}{/Control}"); expect(textarea.textContent).toEqual(""); - await user.keyboard("{Shift>}{Control>}{z}"); + await user.keyboard("{Shift>}{Control>}{Z}"); expect(textarea.textContent).toEqual("@"); - await user.keyboard("{z}"); + + await user.keyboard("{Z}"); expect(textarea.textContent).toEqual("@file "); - await user.keyboard("{z}{/Control}{/Shift}"); + await pause(100); // required, because of cancelling on frequent paste + + await user.keyboard("{z}{/Shift}{/Control}"); expect(textarea.textContent).toEqual("@file /foo "); }); diff --git a/refact-agent/gui/src/components/ComboBox/ComboBox.tsx b/refact-agent/gui/src/components/ComboBox/ComboBox.tsx index c4638bef8..c26d013c2 100644 --- a/refact-agent/gui/src/components/ComboBox/ComboBox.tsx +++ b/refact-agent/gui/src/components/ComboBox/ComboBox.tsx @@ -36,6 +36,7 @@ export const ComboBox: React.FC = ({ }) => { const ref = React.useRef(null); const [moveCursorTo, setMoveCursorTo] = React.useState(null); + const [lastPasteTimestamp, setLastPasteTimestamp] = React.useState(0); const shiftEnterToSubmit = useAppSelector(selectSubmitOption); const { escapeKeyPressed } = useEventsBusForIDE(); @@ -198,9 +199,30 @@ export const ComboBox: React.FC = ({ const handleChange = useCallback( (event: React.ChangeEvent) => { - onChange(event.target.value); + const newValue = event.target.value; + const nativeEvent = event.nativeEvent as InputEvent; + const currentEventTimestamp = nativeEvent.timeStamp; + + const inputType = nativeEvent.inputType; + const isPasteEvent = [ + "insertFromPaste", + "insertFromDrop", + "insertFromYank", + "insertReplacementText", + ].includes(inputType); + + const timeSinceLastChange = currentEventTimestamp - lastPasteTimestamp; + + if (isPasteEvent && timeSinceLastChange < 100) return; + + if (isPasteEvent) { + setLastPasteTimestamp(currentEventTimestamp); + closeCombobox(); + requestCommandsCompletion.cancel(); + } + onChange(newValue); }, - [onChange], + [onChange, closeCombobox, requestCommandsCompletion, lastPasteTimestamp], ); const onItemClick = useCallback( @@ -240,7 +262,7 @@ export const ComboBox: React.FC = ({ showOnChange={false} showOnKeyDown={false} showOnMouseDown={false} - setValueOnChange={true} + setValueOnChange={false} render={render({ ref, placeholder, diff --git a/refact-agent/gui/src/components/Markdown/CodeBlock.tsx b/refact-agent/gui/src/components/Markdown/CodeBlock.tsx index fd7e107f1..f4013b97f 100644 --- a/refact-agent/gui/src/components/Markdown/CodeBlock.tsx +++ b/refact-agent/gui/src/components/Markdown/CodeBlock.tsx @@ -33,6 +33,7 @@ const _MarkdownCodeBlock: React.FC = ({ onCopyClick, wrap = false, color = undefined, + useInlineStyles, }) => { const codeRef = React.useRef(null); const match = /language-(\w+)/.exec(className ?? ""); @@ -71,7 +72,7 @@ const _MarkdownCodeBlock: React.FC = ({ /> )} language={language} - // useInlineStyles={false} + useInlineStyles={useInlineStyles} > {textWithOutIndent ? textWithOutIndent : "No content"} diff --git a/refact-agent/gui/src/components/Markdown/Markdown.tsx b/refact-agent/gui/src/components/Markdown/Markdown.tsx index c5861c559..2c338a07a 100644 --- a/refact-agent/gui/src/components/Markdown/Markdown.tsx +++ b/refact-agent/gui/src/components/Markdown/Markdown.tsx @@ -1,11 +1,4 @@ -import React, { - Key, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import React, { Key, useMemo } from "react"; import ReactMarkdown, { Components } from "react-markdown"; import remarkBreaks from "remark-breaks"; import classNames from "classnames"; @@ -25,26 +18,17 @@ import { Link, Quote, Strong, - Button, Flex, - Card, Table, } from "@radix-ui/themes"; import rehypeKatex from "rehype-katex"; import remarkMath from "remark-math"; import remarkGfm from "remark-gfm"; import "katex/dist/katex.min.css"; -import { useAppSelector, useLinksFromLsp, usePatchActions } from "../../hooks"; +import { useLinksFromLsp } from "../../hooks"; -import { ErrorCallout, DiffWarningCallout } from "../Callout"; - -import { TruncateLeft } from "../Text"; -import { extractFilePathFromPin } from "../../utils"; - -import { telemetryApi } from "../../services/refact/telemetry"; import { ChatLinkButton } from "../ChatLinks"; import { extractLinkFromPuzzle } from "../../utils/extractLinkFromPuzzle"; -import { selectAutomaticPatch, selectToolUse } from "../../features/Chat"; export type MarkdownProps = Pick< React.ComponentProps, @@ -62,142 +46,6 @@ export type MarkdownProps = Pick< wrap?: boolean; } & Partial; -const PinMessages: React.FC<{ - children: string; -}> = ({ children }) => { - const ref = useRef(null); - const { - handleShow, - errorMessage, - resetErrorMessage, - disable, - openFile, - handlePaste, - canPaste, - } = usePatchActions(); - const [sendTelemetryEvent] = - telemetryApi.useLazySendTelemetryChatEventQuery(); - - const toolUse = useAppSelector(selectToolUse); - const isPatchAutomatic = useAppSelector(selectAutomaticPatch); - - const shouldInteractiveButtonsBeVisible = useMemo(() => { - if (toolUse === "agent") return !isPatchAutomatic; - return true; - }, [isPatchAutomatic, toolUse]); - - const getMarkdown = useCallback(() => { - return ( - ref.current?.parentElement?.nextElementSibling?.querySelector("code") - ?.textContent ?? null - ); - }, []); - - const onDiffClick = useCallback(() => { - const markdown = getMarkdown(); - if (markdown) { - handlePaste(markdown); - } - - void sendTelemetryEvent({ - scope: `replaceSelection`, - success: true, - error_message: "", - }); - }, [getMarkdown, handlePaste, sendTelemetryEvent]); - - const handleAutoApply = useCallback( - ( - event: React.MouseEvent, - children: string, - filePath: string, - ) => { - event.preventDefault(); - openFile({ file_name: filePath }); - // timeout is required to open file properly and then start rainbow animation - const timeoutId = setTimeout(() => { - handleShow(children); - clearTimeout(timeoutId); - }, 150); - }, - [handleShow, openFile], - ); - - const [hasMarkdown, setHasMarkdown] = useState(false); - - useEffect(() => { - if (!ref.current) { - setHasMarkdown(false); - } else { - const markdown = !!getMarkdown(); - setHasMarkdown(markdown); - } - }, [getMarkdown]); - - if (children.startsWith("📍OTHER")) { - return null; - } - - const filePath = extractFilePathFromPin(children); - - return ( - - - - { - event.preventDefault(); - openFile({ file_name: filePath }); - }} - > - {filePath} - - {" "} -
- {shouldInteractiveButtonsBeVisible && ( - <> - - - - )} - - {errorMessage && errorMessage.type === "error" && ( - - {errorMessage.text} - - )} - {errorMessage && errorMessage.type === "warning" && ( - - )} - - ); -}; - const PuzzleLink: React.FC<{ children: string; }> = ({ children }) => { @@ -218,10 +66,6 @@ const MaybeInteractiveElement: React.FC<{ children?: React.ReactNode; }> = ({ children }) => { const processed = React.Children.map(children, (child, index) => { - if (typeof child === "string" && child.startsWith("📍")) { - const key = `pin-message-${index}`; - return {child}; - } if (typeof child === "string" && child.startsWith("🧩")) { const key = `puzzle-link-${index}`; return {child}; diff --git a/refact-agent/gui/src/components/TextArea/TextArea.tsx b/refact-agent/gui/src/components/TextArea/TextArea.tsx index e51dda08c..939d76eb1 100644 --- a/refact-agent/gui/src/components/TextArea/TextArea.tsx +++ b/refact-agent/gui/src/components/TextArea/TextArea.tsx @@ -28,13 +28,15 @@ export const TextArea = React.forwardRef( const handleKeyDown = useCallback( (event: React.KeyboardEvent) => { const isMod = event.metaKey || event.ctrlKey; - if (isMod && event.key === "z" && !event.shiftKey) { + const eventKey = event.key.toLowerCase(); + + if (isMod && eventKey === "z" && !event.shiftKey) { event.preventDefault(); undoRedo.undo(); setCallChange(true); } - if (isMod && event.key === "z" && event.shiftKey) { + if (isMod && eventKey === "z" && event.shiftKey) { event.preventDefault(); undoRedo.redo(); setCallChange(true); diff --git a/refact-agent/gui/src/components/Tools/Texdoc.module.css b/refact-agent/gui/src/components/Tools/Texdoc.module.css new file mode 100644 index 000000000..fb6fabb2d --- /dev/null +++ b/refact-agent/gui/src/components/Tools/Texdoc.module.css @@ -0,0 +1,47 @@ +.textdoc { +} + +.textdoc__header { + padding-left: var(--space-2); + padding-right: var(--space-2); +} + +.textdoc__header, +.textdoc__header::before, +.textdoc__header::after { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + border-bottom-width: 0; +} + +.textdoc pre { + margin-top: 0; +} + +:global(.radix-themes) .textdoc.textdoc__update :global(.hljs.language-diff) { + --hlbg: var(--gray-3); + --hlcolor1: var(--gray-12); + /* --hlcolor2: #000000; + --hlcolor3: #000080; + --hlcolor4: #800080; + --hlcolor5: #808000; + --hlcolor6: #800000; + --hlcolor7: #0055af; */ + --hlcolor8: var(--green-9); + --hlcolor9: var(--red-9); +} +:global(.radix-themes) + .textdoc.textdoc__update + :global(.hljs.language-diff .hljs-comment) { + color: var(--hlcolor1); +} + +:global(.radix-themes) + .textdoc.textdoc__update + :global(.hljs.language-diff .hljs-meta) { + color: var(--hlcolor1); +} + +.textdoc__update .textdoc__diffbox { + box-shadow: var(--shadow-1); +} diff --git a/refact-agent/gui/src/components/Tools/Textdoc.tsx b/refact-agent/gui/src/components/Tools/Textdoc.tsx new file mode 100644 index 000000000..17b63209a --- /dev/null +++ b/refact-agent/gui/src/components/Tools/Textdoc.tsx @@ -0,0 +1,263 @@ +import React, { useCallback, useMemo, useState } from "react"; +import { + type CreateTextDocToolCall, + type RawTextDocTool, + ReplaceTextDocToolCall, + TextDocToolCall, + UpdateRegexTextDocToolCall, + UpdateTextDocToolCall, + isCreateTextDocToolCall, + isReplaceTextDocToolCall, + isUpdateRegexTextDocToolCall, + isUpdateTextDocToolCall, + parseRawTextDocToolCall, +} from "./types"; +import { Box, Button, Card, Flex } from "@radix-ui/themes"; +import { TruncateLeft } from "../Text"; +import { Link } from "../Link"; +import { useEventsBusForIDE } from "../../hooks/useEventBusForIDE"; +import { Markdown } from "../Markdown"; +import { filename } from "../../utils/filename"; +import styles from "./Texdoc.module.css"; +import { createPatch } from "diff"; +import classNames from "classnames"; +import { useAppSelector } from "../../hooks"; +import { selectCanPaste } from "../../features/Chat"; +import { toolsApi } from "../../services/refact"; +import { ErrorCallout } from "../Callout"; +import { isRTKResponseErrorWithDetailMessage } from "../../utils"; + +export const TextDocTool: React.FC<{ toolCall: RawTextDocTool }> = ({ + toolCall, +}) => { + const maybeTextDocToolCall = parseRawTextDocToolCall(toolCall); + + if (!maybeTextDocToolCall) return false; + + if (isCreateTextDocToolCall(maybeTextDocToolCall)) { + return ; + } + + if (isUpdateTextDocToolCall(maybeTextDocToolCall)) { + return ; + } + + if (isReplaceTextDocToolCall(maybeTextDocToolCall)) { + return ; + } + + if (isUpdateRegexTextDocToolCall(maybeTextDocToolCall)) { + return ; + } + + return false; +}; + +const TextDocHeader: React.FC<{ + toolCall: TextDocToolCall; +}> = ({ toolCall }) => { + const { openFile, diffPasteBack, sendToolEditToIde } = useEventsBusForIDE(); + const [requestDryRun, dryRunResult] = toolsApi.useDryRunForEditToolMutation(); + const [errorMessage, setErrorMessage] = useState(""); + const canPaste = useAppSelector(selectCanPaste); + + const clearErrorMessage = useCallback(() => setErrorMessage(""), []); + // move this + const handleOpenFile = useCallback(() => { + if (!toolCall.function.arguments.path) return; + openFile({ file_name: toolCall.function.arguments.path }); + }, [openFile, toolCall.function.arguments.path]); + + const handleReplace = useCallback( + (content: string) => { + diffPasteBack(content); + }, + [diffPasteBack], + ); + + const replaceContent = useMemo(() => { + if (isCreateTextDocToolCall(toolCall)) + return toolCall.function.arguments.content; + if (isUpdateTextDocToolCall(toolCall)) + return toolCall.function.arguments.replacement; + return null; + }, [toolCall]); + + const handleApplyToolResult = useCallback(() => { + requestDryRun({ + toolName: toolCall.function.name, + toolArgs: toolCall.function.arguments, + }) + .then((results) => { + if (results.data) { + sendToolEditToIde(toolCall.function.arguments.path, results.data); + } else if (isRTKResponseErrorWithDetailMessage(results)) { + setErrorMessage(results.error.data.detail); + } + }) + .catch((error: unknown) => { + if ( + error && + typeof error === "object" && + "message" in error && + typeof error.message === "string" + ) { + setErrorMessage(error.message); + } else { + setErrorMessage("Error with patch: " + JSON.stringify(error)); + } + }); + }, [ + requestDryRun, + sendToolEditToIde, + toolCall.function.arguments, + toolCall.function.name, + ]); + + return ( + + + + { + event.preventDefault(); + handleOpenFile(); + }} + > + {toolCall.function.arguments.path} + + {" "} +
+ + {replaceContent && ( + + )} + + {errorMessage && ( + + {errorMessage} + + )} + + ); +}; + +const CreateTextDoc: React.FC<{ + toolCall: CreateTextDocToolCall; +}> = ({ toolCall }) => { + const code = useMemo(() => { + const extension = getFileExtension(toolCall.function.arguments.path); + return ( + "```" + extension + "\n" + toolCall.function.arguments.content + "\n```" + ); + }, [toolCall.function.arguments.content, toolCall.function.arguments.path]); + return ( + // TODO: move this box up a bit, or make it generic + + + {code} + + ); +}; + +const ReplaceTextDoc: React.FC<{ + toolCall: ReplaceTextDocToolCall; +}> = ({ toolCall }) => { + const code = useMemo(() => { + const extension = getFileExtension(toolCall.function.arguments.path); + return ( + "```" + + extension + + "\n" + + toolCall.function.arguments.replacement + + "\n```" + ); + }, [ + toolCall.function.arguments.path, + toolCall.function.arguments.replacement, + ]); + return ( + // TODO: move this box up a bit, or make it generic + + + {code} + + ); +}; + +const UpdateRegexTextDoc: React.FC<{ + toolCall: UpdateRegexTextDocToolCall; +}> = ({ toolCall }) => { + const code = useMemo(() => { + return ( + '```py\nre.sub("' + + toolCall.function.arguments.pattern + + '", "' + + toolCall.function.arguments.replacement + + '", open("' + + toolCall.function.arguments.path + + '"))\n```' + ); + }, [ + toolCall.function.arguments.path, + toolCall.function.arguments.pattern, + toolCall.function.arguments.replacement, + ]); + + return ( + + + {code} + + ); +}; + +const UpdateTextDoc: React.FC<{ + toolCall: UpdateTextDocToolCall; +}> = ({ toolCall }) => { + const diff = useMemo(() => { + const patch = createPatch( + toolCall.function.arguments.path, + toolCall.function.arguments.old_str, + toolCall.function.arguments.replacement, + ); + + return "```diff\n" + patch + "\n```"; + }, [ + toolCall.function.arguments.replacement, + toolCall.function.arguments.old_str, + toolCall.function.arguments.path, + ]); + // TODO: don't use markdown for this, it's two bright + return ( + + + + {diff} + + + ); +}; + +function getFileExtension(filePath: string): string { + const fileName = filename(filePath); + if (fileName.toLocaleLowerCase().startsWith("dockerfile")) + return "dockerfile"; + const parts = fileName.split("."); + return parts[parts.length - 1].toLocaleLowerCase(); +} diff --git a/refact-agent/gui/src/components/Tools/types.ts b/refact-agent/gui/src/components/Tools/types.ts new file mode 100644 index 000000000..17a073814 --- /dev/null +++ b/refact-agent/gui/src/components/Tools/types.ts @@ -0,0 +1,181 @@ +import { ToolCall } from "../../services/refact"; +import { parseOrElse } from "../../utils"; + +export const TEXTDOC_TOOL_NAMES = [ + "create_textdoc", + "update_textdoc", + "replace_textdoc", + "update_textdoc_regex", +]; + +type TextDocToolNames = (typeof TEXTDOC_TOOL_NAMES)[number]; + +export interface RawTextDocTool extends ToolCall { + function: { + name: TextDocToolNames; + arguments: string; // stringified json + }; +} + +// To use when deciding to render TextDoc +export const isRawTextDocToolCall = ( + toolCall: ToolCall, +): toolCall is RawTextDocTool => { + if (!toolCall.function.name) return false; + if (typeof toolCall.function.arguments !== "string") return false; + return TEXTDOC_TOOL_NAMES.includes(toolCall.function.name); +}; + +export type ParsedRawTextDocToolCall = Omit & { + function: { + name: TextDocToolNames; + arguments: Record; + }; +}; + +export const isParseRawTextDocToolCall = ( + json: unknown, +): json is ParsedRawTextDocToolCall => { + if (!json) return false; + if (typeof json !== "object") return false; + if (!("function" in json)) return false; + if (!json.function) return false; + if (typeof json.function !== "object") return false; + if (!("name" in json.function)) return false; + if (!json.function.name) return false; + if (!("arguments" in json.function)) return false; + if (!json.function.arguments) return false; + if (typeof json.function.arguments !== "object") return false; + return true; +}; + +export interface CreateTextDocToolCall extends ParsedRawTextDocToolCall { + function: { + name: "create_textdoc"; + arguments: { + path: string; + content: string; + }; + }; +} + +export const isCreateTextDocToolCall = ( + toolCall: ParsedRawTextDocToolCall, +): toolCall is CreateTextDocToolCall => { + if (toolCall.function.name !== "create_textdoc") return false; + if (!("path" in toolCall.function.arguments)) return false; + if (typeof toolCall.function.arguments.path !== "string") return false; + if (!("content" in toolCall.function.arguments)) return false; + if (typeof toolCall.function.arguments.content !== "string") return false; + return true; +}; + +export interface UpdateTextDocToolCall extends ParsedRawTextDocToolCall { + function: { + name: "update_textdoc"; + arguments: { + path: string; + old_str: string; + replacement: string; + multiple: boolean; + }; + }; +} + +export const isUpdateTextDocToolCall = ( + toolCall: ParsedRawTextDocToolCall, +): toolCall is UpdateTextDocToolCall => { + if (toolCall.function.name !== "update_textdoc") return false; + if (!("path" in toolCall.function.arguments)) return false; + if (typeof toolCall.function.arguments.path !== "string") return false; + if (!("old_str" in toolCall.function.arguments)) return false; + if (typeof toolCall.function.arguments.old_str !== "string") return false; + if (!("replacement" in toolCall.function.arguments)) return false; + if (typeof toolCall.function.arguments.replacement !== "string") return false; + if (!("multiple" in toolCall.function.arguments)) return false; + if (typeof toolCall.function.arguments.multiple !== "boolean") return false; + return true; +}; + +export interface UpdateRegexTextDocToolCall extends ParsedRawTextDocToolCall { + function: { + name: string; + arguments: { + path: string; + pattern: string; + replacement: string; + multiple: boolean; + }; + }; +} + +export const isUpdateRegexTextDocToolCall = ( + toolCall: ParsedRawTextDocToolCall, +): toolCall is UpdateRegexTextDocToolCall => { + if (toolCall.function.name !== "update_textdoc_regex") return false; + if (!("path" in toolCall.function.arguments)) return false; + if (typeof toolCall.function.arguments.path !== "string") return false; + if (!("pattern" in toolCall.function.arguments)) return false; + if (typeof toolCall.function.arguments.pattern !== "string") return false; + if (!("replacement" in toolCall.function.arguments)) return false; + if (typeof toolCall.function.arguments.replacement !== "string") return false; + if (!("multiple" in toolCall.function.arguments)) return false; + if (typeof toolCall.function.arguments.multiple !== "boolean") return false; + return true; +}; + +export interface ReplaceTextDocToolCall extends ParsedRawTextDocToolCall { + function: { + name: string; + arguments: { + path: string; + replacement: string; + }; + }; +} + +export const isReplaceTextDocToolCall = ( + toolCall: ParsedRawTextDocToolCall, +): toolCall is ReplaceTextDocToolCall => { + if (toolCall.function.name !== "replace_textdoc") return false; + if (!("path" in toolCall.function.arguments)) return false; + if (typeof toolCall.function.arguments.path !== "string") return false; + if (!("replacement" in toolCall.function.arguments)) return false; + if (typeof toolCall.function.arguments.replacement !== "string") return false; + return true; +}; + +export type TextDocToolCall = + | CreateTextDocToolCall + | UpdateTextDocToolCall + | ReplaceTextDocToolCall + | UpdateRegexTextDocToolCall; + +function isTextDocToolCall( + toolCall: ParsedRawTextDocToolCall, +): toolCall is TextDocToolCall { + if (isCreateTextDocToolCall(toolCall)) return true; + if (isUpdateTextDocToolCall(toolCall)) return true; + if (isReplaceTextDocToolCall(toolCall)) return true; + if (isUpdateRegexTextDocToolCall(toolCall)) return true; + return true; +} + +export function parseRawTextDocToolCall( + toolCall: RawTextDocTool, +): TextDocToolCall | null { + const parsedArguments = parseOrElse>( + toolCall.function.arguments, + {}, + ); + const parsedToolCallWithArgs = { + ...toolCall, + function: { ...toolCall.function, arguments: parsedArguments }, + }; + + if (!isParseRawTextDocToolCall(parsedToolCallWithArgs)) return null; + + if (!isTextDocToolCall(parsedToolCallWithArgs)) return null; + + return parsedToolCallWithArgs; +} diff --git a/refact-agent/gui/src/events/index.ts b/refact-agent/gui/src/events/index.ts index 4b4eb8d5f..0f3de6e46 100644 --- a/refact-agent/gui/src/events/index.ts +++ b/refact-agent/gui/src/events/index.ts @@ -17,7 +17,6 @@ import type { TipOfTheDayState } from "../features/TipOfTheDay"; import type { PageSliceState } from "../features/Pages/pagesSlice"; import type { TourState } from "../features/Tour"; import type { FIMDebugState } from "../hooks"; -import { createAction } from "@reduxjs/toolkit"; import { CurrentProjectInfo } from "../features/Chat/currentProject"; export { updateConfig, type Config } from "../features/Config/configSlice"; @@ -29,7 +28,6 @@ export { export type { FimDebugData } from "../services/refact/fim"; export type { ChatHistoryItem } from "../features/History/historySlice"; export { addInputValue, setInputValue } from "../components/ChatForm/actions"; -export { resetDiffApi } from "../services/refact/diffs"; export { setCurrentProjectInfo, type CurrentProjectInfo, @@ -38,16 +36,14 @@ export { export type { ToolCommand, CustomPromptsResponse, - DiffPreviewResponse, - PatchResult, CapsResponse, UserMessage, ChatMessage, ChatMessages, + DiffChunk, + ToolEditResult, } from "../services/refact"; -export const showPatchTicket = createAction("showPatchTicket"); - // TODO: re-exporting from redux seems to break things :/ export type InitialState = { fim: FIMDebugState; @@ -67,18 +63,17 @@ export { ideOpenFile, type OpenFilePayload, ideDiffPasteBackAction, - ideDiffPreviewAction, ideNewFileAction, ideOpenHotKeys, ideOpenSettingsAction, ideOpenChatInNewTab, ideAnimateFileStart, ideAnimateFileStop, - ideWriteResultsToFile, ideChatPageChange, ideEscapeKeyPressed, ideIsChatStreaming, ideIsChatReady, + ideToolEdit, } from "../hooks/useEventBusForIDE"; export const fim = { diff --git a/refact-agent/gui/src/features/Chat/activeFile.ts b/refact-agent/gui/src/features/Chat/activeFile.ts index df9fbcec3..43bc65331 100644 --- a/refact-agent/gui/src/features/Chat/activeFile.ts +++ b/refact-agent/gui/src/features/Chat/activeFile.ts @@ -32,3 +32,4 @@ export const activeFileReducer = createReducer(initialState, (builder) => { }); export const selectActiveFile = (state: RootState) => state.active_file; +export const selectCanPaste = (state: RootState) => state.active_file.can_paste; diff --git a/refact-agent/gui/src/hooks/index.ts b/refact-agent/gui/src/hooks/index.ts index 49b5b9496..4d5932835 100644 --- a/refact-agent/gui/src/hooks/index.ts +++ b/refact-agent/gui/src/hooks/index.ts @@ -20,7 +20,6 @@ export * from "./useConfig"; export * from "./useAppDispatch"; export * from "./useAppSelector"; export * from "./useSendChatRequest"; -export * from "./usePatchActions"; export * from "./useGetUserSurvey"; export * from "./useLinksFromLsp"; export * from "./useGoToLink"; diff --git a/refact-agent/gui/src/hooks/useEventBusForApp.ts b/refact-agent/gui/src/hooks/useEventBusForApp.ts index 24def3bfb..e0c19b5b6 100644 --- a/refact-agent/gui/src/hooks/useEventBusForApp.ts +++ b/refact-agent/gui/src/hooks/useEventBusForApp.ts @@ -12,15 +12,11 @@ import { push, selectPages, } from "../features/Pages/pagesSlice"; -import { diffApi, resetDiffApi } from "../services/refact/diffs"; -import { usePatchActions } from "./usePatchActions"; -import { showPatchTicket } from "../events"; export function useEventBusForApp() { const config = useConfig(); const dispatch = useAppDispatch(); const pages = useAppSelector(selectPages); - const { handleShow } = usePatchActions(); useEffect(() => { const listener = (event: MessageEvent) => { @@ -43,14 +39,6 @@ export function useEventBusForApp() { dispatch(newChatAction(event.data.payload)); } - if (resetDiffApi.match(event.data)) { - dispatch(diffApi.util.resetApiState()); - } - - if (showPatchTicket.match(event.data)) { - handleShow(event.data.payload); - } - if (setCurrentProjectInfo.match(event.data)) { dispatch(setCurrentProjectInfo(event.data.payload)); } @@ -65,5 +53,5 @@ export function useEventBusForApp() { return () => { window.removeEventListener("message", listener); }; - }, [config.host, dispatch, handleShow, pages]); + }, [config.host, dispatch, pages]); } diff --git a/refact-agent/gui/src/hooks/useEventBusForIDE.ts b/refact-agent/gui/src/hooks/useEventBusForIDE.ts index f43f50eec..c6cc54c33 100644 --- a/refact-agent/gui/src/hooks/useEventBusForIDE.ts +++ b/refact-agent/gui/src/hooks/useEventBusForIDE.ts @@ -7,14 +7,9 @@ import { HostSettings, SetupHost, } from "../events/setup"; -import type { DiffPreviewResponse, PatchResult } from "../services/refact"; export const ideDiffPasteBackAction = createAction("ide/diffPasteBack"); -export const ideDiffPreviewAction = createAction< - DiffPreviewResponse & { currentPin: string; allPins: string[] } ->("ide/diffPreview"); - export const ideOpenSettingsAction = createAction("ide/openSettings"); export const ideNewFileAction = createAction("ide/newFile"); @@ -37,21 +32,24 @@ export const ideAnimateFileStart = createAction( export const ideAnimateFileStop = createAction("ide/animateFile/stop"); -export const ideWriteResultsToFile = createAction( - "ide/writeResultsToFile", -); - export const ideChatPageChange = createAction("ide/chatPageChange"); export const ideEscapeKeyPressed = createAction("ide/escapeKeyPressed"); export const ideIsChatStreaming = createAction("ide/isChatStreaming"); export const ideIsChatReady = createAction("ide/isChatReady"); +export const ideToolEdit = createAction<{ path: string; edit: ToolEditResult }>( + "ide/toolEdit", +); + import { pathApi } from "../services/refact/path"; import { telemetryApi } from "../services/refact/telemetry"; +import { ToolEditResult } from "../services/refact"; export const useEventsBusForIDE = () => { + const [sendTelemetryEvent] = + telemetryApi.useLazySendTelemetryChatEventQuery(); const postMessage = usePostMessage(); // const canPaste = useAppSelector((state) => state.active_file.can_paste); @@ -89,15 +87,13 @@ export const useEventsBusForIDE = () => { (content: string) => { const action = ideDiffPasteBackAction(content); postMessage(action); + void sendTelemetryEvent({ + scope: `replaceSelection`, + success: true, + error_message: "", + }); }, - [postMessage], - ); - - const diffPreview = useCallback( - (preview: DiffPreviewResponse, currentPin: string, allPins: string[]) => { - postMessage(ideDiffPreviewAction({ ...preview, currentPin, allPins })); - }, - [postMessage], + [postMessage, sendTelemetryEvent], ); const openSettings = useCallback(() => { @@ -146,14 +142,6 @@ export const useEventsBusForIDE = () => { [postMessage], ); - const writeResultsToFile = useCallback( - (results: PatchResult[]) => { - const action = ideWriteResultsToFile(results); - postMessage(action); - }, - [postMessage], - ); - const chatPageChange = useCallback( (page: string) => { const action = ideChatPageChange(page); @@ -190,8 +178,6 @@ export const useEventsBusForIDE = () => { const [getIntegrationsPath] = pathApi.useLazyIntegrationsPathQuery(); const [getPrivacyPath] = pathApi.useLazyPrivacyPathQuery(); const [getBringYourOwnKeyPath] = pathApi.useLazyBringYourOwnKeyPathQuery(); - const [sendTelemetryEvent] = - telemetryApi.useLazySendTelemetryChatEventQuery(); // Creating a generic function to trigger different queries from RTK Query (to avoid duplicative code) const openFileFromPathQuery = useCallback( @@ -231,6 +217,14 @@ export const useEventsBusForIDE = () => { const openBringYourOwnKeyFile = () => openFileFromPathQuery(getBringYourOwnKeyPath); + const sendToolEditToIde = useCallback( + (path: string, edit: ToolEditResult) => { + const action = ideToolEdit({ path, edit }); + postMessage(action); + }, + [postMessage], + ); + return { diffPasteBack, openSettings, @@ -239,19 +233,17 @@ export const useEventsBusForIDE = () => { openFile, openChatInNewTab, setupHost, - diffPreview, queryPathThenOpenFile, openCustomizationFile, openPrivacyFile, openBringYourOwnKeyFile, openIntegrationsFile, - // canPaste, stopFileAnimation, startFileAnimation, - writeResultsToFile, chatPageChange, escapeKeyPressed, setIsChatStreaming, setIsChatReady, + sendToolEditToIde, }; }; diff --git a/refact-agent/gui/src/hooks/useLinksFromLsp.ts b/refact-agent/gui/src/hooks/useLinksFromLsp.ts index c2097a395..53ea635b4 100644 --- a/refact-agent/gui/src/hooks/useLinksFromLsp.ts +++ b/refact-agent/gui/src/hooks/useLinksFromLsp.ts @@ -1,6 +1,5 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { - diffApi, isCommitLink, isPostChatLink, isUserMessage, @@ -97,8 +96,6 @@ export function useLinksFromLsp() { const { handleGoTo } = useGoToLink(); const { submit } = useSendChatRequest(); - const [applyPatches, _applyPatchesResult] = - diffApi.useApplyAllPatchesInMessagesMutation(); const [applyCommit, _applyCommitResult] = linksApi.useSendCommitMutation(); const [sendTelemetryEvent] = @@ -179,11 +176,15 @@ export function useLinksFromLsp() { } if (link.link_action === "patch-all") { - void applyPatches(messages).then(() => { - if ("link_goto" in link) { - handleGoTo({ goto: link.link_goto }); - } - }); + // TBD: smart links for patches + // void applyPatches(messages).then(() => { + // if ("link_goto" in link) { + // handleGoTo({ goto: link.link_goto }); + // } + // }); + if ("link_goto" in link) { + handleGoTo({ goto: link.link_goto }); + } return; } @@ -262,15 +263,7 @@ export function useLinksFromLsp() { // eslint-disable-next-line no-console console.warn(`unknown action: ${JSON.stringify(link)}`); }, - [ - applyCommit, - applyPatches, - dispatch, - handleGoTo, - messages, - submit, - sendTelemetryEvent, - ], + [applyCommit, dispatch, handleGoTo, sendTelemetryEvent, submit], ); const linksResult = useGetLinksFromLsp(); diff --git a/refact-agent/gui/src/hooks/usePatchActions.ts b/refact-agent/gui/src/hooks/usePatchActions.ts deleted file mode 100644 index 0a37bf9fd..000000000 --- a/refact-agent/gui/src/hooks/usePatchActions.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { useState, useCallback, useMemo } from "react"; -import { useSelector } from "react-redux"; -import { - AssistantMessage, - diffApi, - isAssistantMessage, - isDetailMessage, -} from "../services/refact"; -import { - selectMessages, - selectIsStreaming, - selectIsWaiting, - selectActiveFile, - selectSelectedSnippet, -} from "../features/Chat"; -import { useEventsBusForIDE } from "./useEventBusForIDE"; -import { useAppSelector } from "./useAppSelector"; -import { extractFilePathFromPin } from "../utils"; - -export const usePatchActions = () => { - const { - diffPreview, - startFileAnimation, - stopFileAnimation, - openFile, - writeResultsToFile, - diffPasteBack, - } = useEventsBusForIDE(); - const messages = useSelector(selectMessages); - const isStreaming = useSelector(selectIsStreaming); - const isWaiting = useSelector(selectIsWaiting); - - const activeFile = useAppSelector(selectActiveFile); - - const snippet = useAppSelector(selectSelectedSnippet); - - const codeLineCount = useMemo(() => { - if (snippet.code.length === 0) return 0; - return snippet.code.split("\n").filter((str) => str).length; - }, [snippet.code]); - - const canPaste = useMemo( - () => activeFile.can_paste && codeLineCount > 0, - [activeFile.can_paste, codeLineCount], - ); - - const [errorMessage, setErrorMessage] = useState<{ - type: "warning" | "error"; - text: string; - } | null>(null); - - const resetErrorMessage = useCallback(() => { - setErrorMessage(null); - }, []); - - const [getPatch, patchResult] = diffApi.usePatchSingleFileFromTicketMutation( - {}, - ); - - const disable = useMemo(() => { - return !!errorMessage || isStreaming || isWaiting || patchResult.isLoading; - }, [errorMessage, isStreaming, isWaiting, patchResult.isLoading]); - - const pinMessages = useMemo(() => { - const assistantMessages: AssistantMessage[] = - messages.filter(isAssistantMessage); - - const lines = assistantMessages.reduce((acc, curr) => { - if (!curr.content) return acc; - return acc.concat(curr.content.split("\n")); - }, []); - - return lines.filter((line) => line.startsWith("📍")); - }, [messages]); - - const handleShow = useCallback( - (pin: string) => { - const fileName = extractFilePathFromPin(pin); - const cleanedFileName = fileName.replace(/\\\?\\|^\\+/g, ""); - startFileAnimation(cleanedFileName); - getPatch({ pin, messages }) - .unwrap() - .then((maybeDetail) => { - if (isDetailMessage(maybeDetail)) { - const error = new Error(maybeDetail.detail); - throw error; - } - return maybeDetail; - }) - .then((patch) => { - stopFileAnimation(cleanedFileName); - - if (patch.results.every((result) => result.already_applied)) { - const errorText = - "Already applied, no significant changes generated."; - setErrorMessage({ - type: "warning", - text: errorText, - }); - } else { - diffPreview(patch, pin, pinMessages); - } - }) - .catch((error: Error | { data: { detail: string } }) => { - stopFileAnimation(cleanedFileName); - let text = ""; - if ("message" in error) { - text = "Failed to open patch: " + error.message; - } else { - text = "Failed to open patch: " + error.data.detail; - } - setErrorMessage({ - type: "error", - text: text, - }); - }); - }, - [ - diffPreview, - getPatch, - messages, - pinMessages, - startFileAnimation, - stopFileAnimation, - ], - ); - - const handleApply = useCallback( - (pin: string) => { - const fileName = extractFilePathFromPin(pin); - startFileAnimation(fileName); - - getPatch({ pin, messages }) - .unwrap() - .then((maybeDetail) => { - if (isDetailMessage(maybeDetail)) { - const error = new Error(maybeDetail.detail); - throw error; - } - return maybeDetail; - }) - .then((patch) => { - stopFileAnimation(fileName); - if (patch.results.every((result) => result.already_applied)) { - setErrorMessage({ - type: "warning", - text: "Already applied, no significant changes generated.", - }); - } else { - writeResultsToFile(patch.results); - } - }) - .catch((error: Error | { data: { detail: string } }) => { - stopFileAnimation(fileName); - if ("message" in error) { - setErrorMessage({ - type: "error", - text: "Failed to apply patch: " + error.message, - }); - } else { - setErrorMessage({ - type: "error", - text: "Failed to apply patch: " + error.data.detail, - }); - } - }); - }, - [ - getPatch, - messages, - startFileAnimation, - stopFileAnimation, - writeResultsToFile, - ], - ); - - return { - errorMessage, - handleShow, - patchResult, - handleApply, - resetErrorMessage, - disable, - openFile, - - handlePaste: diffPasteBack, - canPaste, - }; -}; diff --git a/refact-agent/gui/src/services/refact/consts.ts b/refact-agent/gui/src/services/refact/consts.ts index d7fd3a07d..dedc0afa8 100644 --- a/refact-agent/gui/src/services/refact/consts.ts +++ b/refact-agent/gui/src/services/refact/consts.ts @@ -7,6 +7,7 @@ export const CUSTOM_PROMPTS_URL = "/v1/customization"; export const AT_TOOLS_AVAILABLE_URL = "/v1/tools"; export const TOOLS_CHECK_CONFIRMATION = "/v1/tools-check-if-confirmation-needed"; +export const EDIT_TOOL_DRY_RUN_URL = "/v1/file_edit_tool_dry_run"; export const CONFIG_PATH_URL = "/v1/config-path"; export const FULL_PATH_URL = "/v1/fullpath"; // TODO: add a service for the docs. diff --git a/refact-agent/gui/src/services/refact/diffs.ts b/refact-agent/gui/src/services/refact/diffs.ts index ac40b731f..f7c448af4 100644 --- a/refact-agent/gui/src/services/refact/diffs.ts +++ b/refact-agent/gui/src/services/refact/diffs.ts @@ -1,228 +1,3 @@ -import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; -import { PATCH_URL, APPLY_ALL_URL } from "./consts"; -import { ChatMessages, DiffChunk, isDiffChunk } from "./types"; -import { RootState } from "../../app/store"; -import { createAction } from "@reduxjs/toolkit"; -import { formatMessagesForLsp } from "../../features/Chat/Thread/utils"; - -type PatchState = { - chunk_id: number; - applied: boolean; - can_unapply: boolean; - success: boolean; - detail: null | string; -}; - -function isPatchState(json: unknown): json is PatchState { - if (!json || typeof json !== "object") return false; - if (!("chunk_id" in json)) return false; - if (typeof json.chunk_id !== "number") return false; - if (!("applied" in json)) return false; - if (typeof json.applied !== "boolean") return false; - if (!("can_unapply" in json)) return false; - if (typeof json.can_unapply !== "boolean") return false; - if (!("success" in json)) return false; - if (typeof json.success !== "boolean") return false; - return true; -} - -export type PatchResult = { - file_text: string; - already_applied: boolean; - file_name_edit: string | null; - file_name_delete: string | null; - file_name_add: string | null; -}; - -function isPatchResult(json: unknown): json is PatchResult { - if (!json || typeof json !== "object") return false; - - if (!("file_text" in json)) return false; - if (typeof json.file_text !== "string") return false; - - if (!("already_applied" in json)) return false; - if (typeof json.already_applied !== "boolean") return false; - - if (!("file_name_edit" in json)) return false; - if (typeof json.file_name_edit !== "string" && json.file_name_edit !== null) { - return false; - } - - if (!("file_name_delete" in json)) return false; - if ( - typeof json.file_name_delete !== "string" && - json.file_name_delete !== null - ) { - return false; - } - - if (!("file_name_add" in json)) return false; - if (typeof json.file_name_add !== "string" && json.file_name_add !== null) { - return false; - } - - return true; -} - -type PatchResponse = { - state: PatchState[]; - results: PatchResult[]; - chunks: DiffChunk[]; -}; - -function isPatchResponse(json: unknown): json is PatchResponse { - if (!json || typeof json !== "object") return false; - if (!("state" in json)) return false; - if (!Array.isArray(json.state)) return false; - if (!json.state.every(isPatchState)) return false; - if (!("results" in json)) return false; - if (!Array.isArray(json.results)) return false; - if (!json.results.every(isPatchResult)) return false; - if (!("chunks" in json)) return false; - if (!Array.isArray(json.chunks)) return false; - if (!json.chunks.every(isDiffChunk)) return false; - return true; -} -type ApplyAllResponse = { - chunks: DiffChunk[]; -}; -function isApplyAllResponse(json: unknown): json is ApplyAllResponse { - if (!json || typeof json !== "object") return false; - if (!("chunks" in json)) return false; - if (!Array.isArray(json.chunks)) return false; - if (!json.chunks.every(isDiffChunk)) return false; - return true; -} - -type PatchRequest = { - pin: string; - messages: ChatMessages; -}; - -export const resetDiffApi = createAction("diffs/reset"); - -export const diffApi = createApi({ - reducerPath: "diffs", - baseQuery: fetchBaseQuery({ - prepareHeaders: (headers, api) => { - const getState = api.getState as () => RootState; - const state = getState(); - const token = state.config.apiKey; - if (token) { - headers.set("Authorization", `Bearer ${token}`); - } - return headers; - }, - }), - endpoints: (builder) => ({ - patchSingleFileFromTicket: builder.mutation({ - async queryFn(args, api, _extraOptions, baseQuery) { - const state = api.getState() as RootState; - const port = state.config.lspPort as unknown as number; - const url = `http://127.0.0.1:${port}${PATCH_URL}`; - - const ticket = args.pin.split(" ")[1] ?? ""; - const messages = formatMessagesForLsp(args.messages); - - const result = await baseQuery({ - url, - credentials: "same-origin", - redirect: "follow", - method: "POST", - body: { - messages, - ticket_ids: [ticket], - }, - }); - - if (result.error) return { error: result.error }; - - if (!isPatchResponse(result.data)) { - return { - error: { - status: "CUSTOM_ERROR", - error: "Failed to parse patch response", - data: result.data, - }, - }; - } - - return { data: result.data }; - }, - }), - - applyAllPatchesInMessages: builder.mutation( - { - async queryFn(messages, api, extraOptions, baseQuery) { - const state = api.getState() as RootState; - const port = state.config.lspPort as unknown as number; - const url = `http://127.0.0.1:${port}${APPLY_ALL_URL}`; - const formattedMessage = formatMessagesForLsp(messages); - const result = await baseQuery({ - ...extraOptions, - url, - credentials: "same-origin", - redirect: "follow", - method: "POST", - body: { - messages: formattedMessage, - }, - }); - - if (result.error) { - return { error: result.error }; - } - - if (!isApplyAllResponse(result.data)) { - return { - error: { - status: "CUSTOM_ERROR", - error: "Failed to parse apply all response", - data: result.data, - }, - }; - } - - return { - data: result.data, - }; - }, - }, - ), - }), -}); - -export interface DiffOperationResponse { - fuzzy_results: { - chunk_id: number; - fuzzy_n_used: number; - }[]; - - state: (0 | 1 | 2)[]; -} - -export type DiffApplyResponse = { - chunk_id: number; - applied: boolean; - can_unapply: boolean; - success: boolean; - detail: null | string; -}[]; - -export type DiffApplyErrorResponse = { - chunk_id: number; - applied: false; - can_unapply: false; - success: false; - detail: null | string; -}; - -export interface DiffPreviewResponse { - state: DiffApplyResponse; - results: { - file_text: string; - file_name_edit: string | null; - file_name_delete: null | string; - file_name_add: null | string; - }[]; -} +// This file has been deprecated. Types have been moved to types.ts. +// The file is kept temporarily to maintain git history. +// TODO: Remove this file after all references have been updated. diff --git a/refact-agent/gui/src/services/refact/index.ts b/refact-agent/gui/src/services/refact/index.ts index 22016649c..6153995d8 100644 --- a/refact-agent/gui/src/services/refact/index.ts +++ b/refact-agent/gui/src/services/refact/index.ts @@ -6,7 +6,6 @@ export * from "./prompts"; export * from "./statistics"; export * from "./tools"; export * from "./types"; -export * from "./diffs"; export * from "./path"; export * from "./ping"; export * from "./links"; diff --git a/refact-agent/gui/src/services/refact/tools.ts b/refact-agent/gui/src/services/refact/tools.ts index 9f64e1f75..c999a7843 100644 --- a/refact-agent/gui/src/services/refact/tools.ts +++ b/refact-agent/gui/src/services/refact/tools.ts @@ -1,7 +1,11 @@ import { RootState } from "../../app/store"; -import { AT_TOOLS_AVAILABLE_URL, TOOLS_CHECK_CONFIRMATION } from "./consts"; +import { + AT_TOOLS_AVAILABLE_URL, + TOOLS_CHECK_CONFIRMATION, + EDIT_TOOL_DRY_RUN_URL, +} from "./consts"; import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; -import { ChatMessage, ToolCall } from "./types"; +import { ChatMessage, DiffChunk, isDiffChunk, ToolCall } from "./types"; import { formatMessagesForLsp } from "../../features/Chat/Thread/utils"; export const toolsApi = createApi({ @@ -82,6 +86,40 @@ export const toolsApi = createApi({ return { data: result.data }; }, }), + dryRunForEditTool: builder.mutation< + ToolEditResult, + { toolName: string; toolArgs: Record } + >({ + async queryFn(args, api, extraOptions, baseQuery) { + const getState = api.getState as () => RootState; + const state = getState(); + const port = state.config.lspPort; + const url = `http://127.0.0.1:${port}${EDIT_TOOL_DRY_RUN_URL}`; + + const response = await baseQuery({ + ...extraOptions, + url, + method: "POST", + body: { tool_name: args.toolName, tool_args: args.toolArgs }, + credentials: "same-origin", + redirect: "follow", + }); + + if (response.error) return response; + + if (!isToolEditResult(response.data)) { + return { + error: { + error: `Invalid response from ${EDIT_TOOL_DRY_RUN_URL}`, + data: response.data, + status: "CUSTOM_ERROR", + }, + }; + } + + return { data: response.data }; + }, + }), }), refetchOnMountOrArgChange: true, }); @@ -147,3 +185,22 @@ export function isToolConfirmationResponse( } return true; } + +export type ToolEditResult = { + file_before: string; + file_after: string; + chunks: DiffChunk[]; +}; + +export function isToolEditResult(data: unknown): data is ToolEditResult { + if (!data) return false; + if (typeof data !== "object") return false; + if (!("file_before" in data)) return false; + if (typeof data.file_before !== "string") return false; + if (!("file_after" in data)) return false; + if (typeof data.file_after !== "string") return false; + if (!("chunks" in data)) return false; + if (!Array.isArray(data.chunks)) return false; + + return data.chunks.every(isDiffChunk); +} diff --git a/refact-agent/gui/src/utils/extractFilePathFromPin.ts b/refact-agent/gui/src/utils/extractFilePathFromPin.ts deleted file mode 100644 index 1e2692dc6..000000000 --- a/refact-agent/gui/src/utils/extractFilePathFromPin.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function extractFilePathFromPin(inputString: string): string { - const start = inputString.indexOf('"') + 1; // Find the first quote and move one character forward - const end = inputString.lastIndexOf('"'); // Find the last quote - if (start !== end) { - return inputString.substring(start, end); // Return the substring between the quotes - } - - // fallback for old messages - const [, , fileName] = inputString.split(" "); - return fileName; -} diff --git a/refact-agent/gui/src/utils/index.ts b/refact-agent/gui/src/utils/index.ts index 83769bc7d..e61b563f8 100644 --- a/refact-agent/gui/src/utils/index.ts +++ b/refact-agent/gui/src/utils/index.ts @@ -6,7 +6,7 @@ export * from "./parseOrElse"; export * from "./takeWhile"; export * from "./takeFromEndWhile"; export * from "./scanForDuplicates"; -export * from "./extractFilePathFromPin"; export * from "./partition"; export * from "./fencedBackticks"; export * from "./isAbsolutePath"; +export * from "./isDetailMessage"; diff --git a/refact-agent/gui/src/utils/isDetailMessage.ts b/refact-agent/gui/src/utils/isDetailMessage.ts new file mode 100644 index 000000000..ef8e9f5b7 --- /dev/null +++ b/refact-agent/gui/src/utils/isDetailMessage.ts @@ -0,0 +1,27 @@ +export type RTKResponseErrorWithDetailMessage = { + error: { + data: { + detail: string; + }; + }; +}; + +export function isRTKResponseErrorWithDetailMessage( + json: unknown, +): json is RTKResponseErrorWithDetailMessage { + const result = + json && + typeof json === "object" && + "error" in json && + json.error && + typeof json.error === "object" && + "data" in json.error && + json.error.data && + typeof json.error.data === "object" && + "detail" in json.error.data && + typeof json.error.data.detail === "string" + ? true + : false; + + return result; +} From 762daf846cab7c413b7d2e21c34c8cf95f6ab7a3 Mon Sep 17 00:00:00 2001 From: Marc McIntosh Date: Tue, 18 Feb 2025 14:36:04 +0100 Subject: [PATCH 2/4] fix(git ignore): git was ignoring __fixtures__ and __tests__ directories. --- .gitignore | 2 + .../gui/src/__fixtures__/chat_textdoc.ts | 1104 +++++++++++++++++ 2 files changed, 1106 insertions(+) create mode 100644 refact-agent/gui/src/__fixtures__/chat_textdoc.ts diff --git a/.gitignore b/.gitignore index 59484fff5..33f24e035 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,8 @@ __pycache__/ *.py[cod] *$py.class _* +!__fixtures__ +!__tests__ # C extensions *.so diff --git a/refact-agent/gui/src/__fixtures__/chat_textdoc.ts b/refact-agent/gui/src/__fixtures__/chat_textdoc.ts new file mode 100644 index 000000000..cd2b13e97 --- /dev/null +++ b/refact-agent/gui/src/__fixtures__/chat_textdoc.ts @@ -0,0 +1,1104 @@ +/*eslint no-irregular-whitespace: ["error", { "skipComments": true }]*/ + +import type { ChatThread } from "../features/Chat/Thread"; +export const CHAT_WITH_TEXTDOC: ChatThread = { + id: "754565e2-8efd-469b-a9bf-1414ce566ff2", + new_chat_suggested: { wasSuggested: false }, + messages: [ + { + role: "system", + content: + "[mode3] You are Refact Agent, an autonomous bot for coding tasks.\n\nCore Principles\n1. Use knowledge()\n - Always use knowledge() first when you encounter an agentic (complex) task.\n - This tool can access external data, including successful “trajectories” (examples of past solutions).\n - External database records begin with the icon “🗃️” followed by a record identifier.\n - Use these records to help solve your tasks by analogy.\n2. Use locate() with the Full Problem Statement\n - Provide the entire user request in the problem_statement argument to avoid losing any details (“telephone game” effect).\n - Include user’s emotional stance, code snippets, formatting, instructions—everything word-for-word.\n - Only omit parts of the user’s request if they are unrelated to the final solution.\n - Avoid using locate() if the problem is quite simple and can be solved without extensive project analysis.\n\nAnswering Strategy\n1. If the user’s question is unrelated to the project\n - Answer directly without using any special calls.\n2. If the user’s question is related to the project\n - First, call knowledge() for relevant information and best practices.\n3. Making Changes\n - If a solution requires file changes, use `*_textdoc()` tools.\n - It's a good practice to call cat() to track changes for changed files.\n\nImportant Notes\n1. Parallel Exploration\n - When you explore different ideas, use multiple parallel methods.\n2. Project-Related Questions\n - For any project question, always call knowledge() before taking any action.\n\nWhen running on user's laptop, you most likely have the shell() tool. It's for one-time dependency installations, or doing whatever\nuser is asking you to do. Tools the user can set up are better, because they don't require confimations when running on a laptop.\nWhen doing something typical for the project, offer the user to make a cmdline_* tool after you have run it.\nYou can do this by writing:\n\n🧩SETTINGS:cmdline_cargo_check\n\nfrom a new line, that will open (when clicked) a wizard that creates `cargo check` (in this example) command line tool.\n\nIn a similar way, service_* tools work. The difference is cmdline_* is designed for non-interactive blocking commands that immediately\nreturn text in stdout/stderr, and service_* is designed for blocking background commands, such as hypercorn server that runs forever until you hit Ctrl+C.\nHere is another example:\n\n🧩SETTINGS:service_hypercorn\n\n\nYou might receive additional instructions that start with 💿. Those are not coming from the user, they are programmed to help you operate\nwell and they are always in English. Answer in the language the user has asked the question.\n\n\n- below general information about the current project -\n\nThe current IDE workspace has these project directories:\n/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation\n\nThere is no active file currently open in the IDE.\nThe project is under git version control, located at:\n/Users/marc/Projects/refact-lsp\n\n\n", + }, + { + role: "user", + content: "Create tests for frog\n", + checkpoints: [ + { + workspace_folder: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation", + commit_hash: "ae0970ff3eca36d1867466847ac876d747357668", + }, + ], + }, + { + role: "assistant", + content: + "I'll help create tests for the frog-related code. Let me first understand the project structure and content.", + tool_calls: [ + { + id: "toolu_01XVhkyaDunsy4fPrDqy3toa", + function: { + arguments: + '{"goal": "Create tests for frog-related code", "language_slash_framework": "rust/tokio", "im_going_to_use_tools": "cat, tree", "im_going_to_apply_to": "emergency_frog_situation, tests"}', + name: "knowledge", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "tool", + content: { + tool_call_id: "toolu_01XVhkyaDunsy4fPrDqy3toa", + content: + "🗃️e19af1e7b3\nYou have a specialization today: web development.\n\nYou only need to receive instructions from the user once, and then you can autonomously fill in the details of\nthe task, make the necessary changes, verify results and make adjustments and fixes.\n\nHere's your approximate web development plan:\n1. Investigate project to understand the task given by the user, start with calling tree() and looking into relevant files. If you see reference designs and sketches, read them using cat().\n2. Run the server. You don't have direct access to the command line. Look if there's a tool for that purpose. If there is not, you cannot run a web server.\n3. Make relevant screenshots of existing website using chrome(), open both desktop and mobile tabs if the task requires it.\n4. Form a complete interpretation of the task, and write a plan.\n5. Make changes in files using 📍-notation, after that call patch(). Really, first you need to write the updates using 📍-notation, only after that you can apply it using patch().\n6. Check if screenshots got better, or any errors appeared.\n7. Goto 5, unless you see the task is complete.\n\nAs a web developer agent, you need to pay attention to detail. The task is complete if all the elements\nare at the right place. You really need to cat() designs and sketches if they are present in the task.\n\nIf you don't see a way to run a real server for the website, then just use chrome() to look\nat .html pages using file:// addresses.\n\nHere is a compressed example of successful trajectory from another project:\n\nDON'T DO STUPID THINGS:\n* DON'T SKIP MAKING SCREENSHOTS\n* DON'T CALL patch() UNTIL YOU FINIHSHED WRITING CODE IN 📍-NOTATION\n* DON'T ASK USER ANYTHING, YOU HAVE AUTONOMOUS WORK TO DO\n* MAKE SURE IF HAVE A TOOL CALL IN THE END OF EACH RESPONSE, UNLESS YOU COMPLETED AND TESTED THE TASK\n\n\n🗃️d84f5c4a7c\nAdditional instructions for django web development.\n\nYou only need to receive instructions from the user once, and then you can autonomously fill in the details of\nthe task, make the necessary changes, verify results and make adjustments and fixes.\n\nHere's your approximate web development plan:\n1. Investigate project to understand the task given by the user, start with calling tree() and locate(), looking into relevant files using cat(). If you see reference designs and sketches, read them using cat()\n2. Start django server\n3. Navigate to the place on the website that user wants to change, make a screenshot to make sure you understand what exactly needs to change\n4. Form a complete interpretation of the task, and write a plan.\n5. Make changes in files using 📍-notation, after that call patch(). Really, first you need to write the updates using 📍-notation, only after that you can apply it.\n6. Check if screenshots got better, or any errors appeared.\n7. Goto 5, unless you see the task is complete.\n\nAs a web developer agent, you need to pay attention to detail. The task is complete if all the elements\nare at the right place.\n\nDON'T DO STUPID THINGS:\n* DON'T SKIP MAKING SCREENSHOTS\n* DON'T CALL patch() UNTIL YOU FINIHSHED WRITING CODE IN 📍-NOTATION\n* DON'T ASK USER ANYTHING, YOU HAVE AUTONOMOUS WORK TO DO\n* MAKE SURE YOU HAVE A TOOL CALL IN THE END OF EACH RESPONSE, UNLESS YOU COMPLETED AND TESTED THE TASK\n\n🗃️ae3f1228bd\n[\n[\"goal\", \"Rename all occurrences of 'frog' to 'bird' in the project\"],\n[\"tree(use_ast=true)\", \"Found emergency_frog_situation/ with index.html, holiday.py, work_day.py, game.js, jump_to_conclusions.py, bird.py, set_as_avatar.py\"],\n[\"search(query='frog', scope='workspace')\", \"Found frog references in work_day.py (imports, function), jump_to_conclusions.py (imports, class usage), bird.py already has Bird class\"],\n[\"thinking\", \"bird.py already has Bird class and set_as_avatar.py uses it, so we need to update work_day.py and jump_to_conclusions.py to use the existing Bird class\"],\n[\"coding\", \"📍REWRITE_WHOLE_FILE 001 'work_day.py' changed import frog->bird, bring_your_own_frog->bring_your_own_bird, frog.Frog->bird.Bird\"],\n[\"patch(tickets='001', path='tests/emergency_frog_situation/work_day.py')\", \"3 chunks applied: import change, function rename, type annotation update\"],\n[\"coding\", \"📍REWRITE_WHOLE_FILE 002 'jump_to_conclusions.py' changed import frog->bird, draw_hello_frog->draw_hello_bird, all frog.Frog->bird.Bird\"],\n[\"patch(tickets='002', path='tests/emergency_frog_situation/jump_to_conclusions.py')\", \"5 chunks applied: import, function rename, constructor call, type annotation, function call\"],\n[\"outcome\", \"SUCCESS\"]\n]\n\n🗃️2b684b6e70\nYou have a specialization today: web development.\n\nYou only need to receive instructions from the user once, and then you can autonomously fill in the details of\nthe task, make the necessary changes, verify results and make adjustments and fixes.\n\nHere's your approximate web development plan:\n1. Investigate project to understand the task given by the user, start with calling tree() and looking into relevant files. If you see reference designs and sketches, read them using cat().\n2. Run the server. You don't have direct access to the command line. Look if there's a tool for that purpose. If there is not, you cannot run a web server.\n3. Make relevant screenshots of existing website using chrome(), open both desktop and mobile tabs if the task requires it.\n4. Form a complete interpretation of the task, and write a plan.\n5. Make changes in files using 📍-notation, after that call patch(). Really, first you need to write the updates using 📍-notation, only after that you can apply it using patch().\n6. Check if screenshots got better, or any errors appeared.\n7. Goto 5, unless you see the task is complete.\n\nAs a web developer agent, you need to pay attention to detail. The task is complete if all the elements\nare at the right place. You really need to cat() designs and sketches if they are present in the task.\n\nIf you don't see a way to run a real server for the website, then just use chrome() to look\nat .html pages using file:// addresses.\n\nHere is a compressed example of successful trajectory from another project:\n\nDON'T DO STUPID THINGS:\n* DON'T SKIP MAKING SCREENSHOTS\n* DON'T CALL patch() UNTIL YOU FINIHSHED WRITING CODE IN 📍-NOTATION\n* DON'T ASK USER ANYTHING, YOU HAVE AUTONOMOUS WORK TO DO\n* MAKE SURE IF HAVE A TOOL CALL IN THE END OF EACH RESPONSE, UNLESS YOU COMPLETED AND TESTED THE TASK\n\n", + }, + }, + { + role: "assistant", + content: + "\n\nLet me check the project structure to understand what we're working with.", + tool_calls: [ + { + id: "toolu_01HMyLgKsLQURM9vgd3vQKXN", + function: { + arguments: '{"use_ast": true}', + name: "tree", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "tool", + content: { + tool_call_id: "toolu_01HMyLgKsLQURM9vgd3vQKXN", + content: + "/\n home/\n svakhreev/\n projects/\n refact-lsp/\n tests/\n emergency_frog_situation/\n holiday.py\n work_day.py\n __pycache__/\n frog.cpython-310.pyc\n frog.py\n jump_to_conclusions.py\n set_as_avatar.py", + }, + }, + { + role: "assistant", + content: + "\n\nLet me examine the frog-related files to understand what we need to test.", + tool_calls: [ + { + id: "toolu_0136dUTkih5ES8rrzHa5B5ep", + function: { + arguments: + '{"paths": "tests/emergency_frog_situation/frog.py", "skeleton": false}', + name: "cat", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "tool", + content: { + tool_call_id: "toolu_0136dUTkih5ES8rrzHa5B5ep", + content: + "Paths found:\n/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/frog.py\n", + }, + }, + { + role: "context_file", + content: [ + { + file_name: "frog.py", + file_content: + 'import numpy as np\n\nDT = 0.01\n\nclass Frog:\n def __init__(self, x, y, vx, vy):\n self.x = x\n self.y = y\n self.vx = vx\n self.vy = vy\n\n def bounce_off_banks(self, pond_width, pond_height):\n if self.x < 0:\n self.vx = np.abs(self.vx)\n elif self.x > pond_width:\n self.vx = -np.abs(self.vx)\n if self.y < 0:\n self.vy = np.abs(self.vy)\n elif self.y > pond_height:\n self.vy = -np.abs(self.vy)\n\n def jump(self, pond_width, pond_height):\n self.x += self.vx * DT\n self.y += self.vy * DT\n self.bounce_off_banks(pond_width, pond_height)\n self.x = np.clip(self.x, 0, pond_width)\n self.y = np.clip(self.y, 0, pond_height)\n\n def croak(self, n_times):\n for n in range(n_times):\n print("croak")\n\n\nclass AlternativeFrog:\n def alternative_jump(self):\n pass\n\n\ndef standalone_jumping_function():\n print("I\'m a frog! Jump! Jump! Really!")\n', + line1: 1, + line2: 39, + }, + ], + }, + { + role: "assistant", + content: + "I'll create tests for the Frog class and its related functionality. We'll need to test the initialization, bouncing behavior, jumping mechanics, and the croak method. Let me create a test file using pytest.", + tool_calls: [ + { + id: "toolu_015isrQXYKSQ4XYqECkXis3J", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", "content": "import pytest\\nimport numpy as np\\nfrom frog import Frog, DT, AlternativeFrog, standalone_jumping_function\\nfrom unittest.mock import patch\\nimport io\\nimport sys\\n\\ndef test_frog_initialization():\\n frog = Frog(1.0, 2.0, 3.0, 4.0)\\n assert frog.x == 1.0\\n assert frog.y == 2.0\\n assert frog.vx == 3.0\\n assert frog.vy == 4.0\\n\\nclass TestFrogBouncing:\\n def test_bounce_off_left_bank(self):\\n frog = Frog(-1.0, 5.0, -2.0, 0.0)\\n frog.bounce_off_banks(10, 10)\\n assert frog.vx > 0 # Should reverse horizontal velocity\\n\\n def test_bounce_off_right_bank(self):\\n frog = Frog(11.0, 5.0, 2.0, 0.0)\\n frog.bounce_off_banks(10, 10)\\n assert frog.vx < 0 # Should reverse horizontal velocity\\n\\n def test_bounce_off_bottom(self):\\n frog = Frog(5.0, -1.0, 0.0, -2.0)\\n frog.bounce_off_banks(10, 10)\\n assert frog.vy > 0 # Should reverse vertical velocity\\n\\n def test_bounce_off_top(self):\\n frog = Frog(5.0, 11.0, 0.0, 2.0)\\n frog.bounce_off_banks(10, 10)\\n assert frog.vy < 0 # Should reverse vertical velocity\\n\\n def test_no_bounce_within_bounds(self):\\n frog = Frog(5.0, 5.0, 1.0, 1.0)\\n original_vx, original_vy = frog.vx, frog.vy\\n frog.bounce_off_banks(10, 10)\\n assert frog.vx == original_vx\\n assert frog.vy == original_vy\\n\\nclass TestFrogJumping:\\n def test_normal_jump(self):\\n frog = Frog(5.0, 5.0, 1.0, 1.0)\\n expected_x = 5.0 + 1.0 * DT\\n expected_y = 5.0 + 1.0 * DT\\n frog.jump(10, 10)\\n assert np.isclose(frog.x, expected_x)\\n assert np.isclose(frog.y, expected_y)\\n\\n def test_jump_with_boundary_clipping(self):\\n frog = Frog(9.9, 9.9, 10.0, 10.0)\\n frog.jump(10, 10)\\n assert frog.x == 10.0 # Should be clipped to pond width\\n assert frog.y == 10.0 # Should be clipped to pond height\\n\\n def test_jump_with_negative_boundary_clipping(self):\\n frog = Frog(0.1, 0.1, -10.0, -10.0)\\n frog.jump(10, 10)\\n assert frog.x == 0.0 # Should be clipped to minimum\\n assert frog.y == 0.0 # Should be clipped to minimum\\n\\ndef test_croak():\\n frog = Frog(0, 0, 0, 0)\\n # Capture stdout\\n captured_output = io.StringIO()\\n sys.stdout = captured_output\\n \\n frog.croak(3)\\n \\n # Restore stdout\\n sys.stdout = sys.__stdout__\\n \\n output = captured_output.getvalue().strip()\\n expected_output = \\"croak\\\\ncroak\\\\ncroak\\"\\n assert output == expected_output\\n\\ndef test_alternative_frog():\\n alt_frog = AlternativeFrog()\\n # Just verify it can be instantiated and method exists\\n alt_frog.alternative_jump()\\n\\ndef test_standalone_jumping_function():\\n captured_output = io.StringIO()\\n sys.stdout = captured_output\\n \\n standalone_jumping_function()\\n \\n sys.stdout = sys.__stdout__\\n \\n output = captured_output.getvalue().strip()\\n assert output == \\"I\'m a frog! Jump! Jump! Really!\\""}', + name: "create_textdoc", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 1, + line2: 1, + lines_remove: "", + lines_add: + 'import pytest\nimport numpy as np\nfrom frog import Frog, DT, AlternativeFrog, standalone_jumping_function\nfrom unittest.mock import patch\nimport io\nimport sys\n\ndef test_frog_initialization():\n frog = Frog(1.0, 2.0, 3.0, 4.0)\n assert frog.x == 1.0\n assert frog.y == 2.0\n assert frog.vx == 3.0\n assert frog.vy == 4.0\n\nclass TestFrogBouncing:\n def test_bounce_off_left_bank(self):\n frog = Frog(-1.0, 5.0, -2.0, 0.0)\n frog.bounce_off_banks(10, 10)\n assert frog.vx > 0 # Should reverse horizontal velocity\n\n def test_bounce_off_right_bank(self):\n frog = Frog(11.0, 5.0, 2.0, 0.0)\n frog.bounce_off_banks(10, 10)\n assert frog.vx < 0 # Should reverse horizontal velocity\n\n def test_bounce_off_bottom(self):\n frog = Frog(5.0, -1.0, 0.0, -2.0)\n frog.bounce_off_banks(10, 10)\n assert frog.vy > 0 # Should reverse vertical velocity\n\n def test_bounce_off_top(self):\n frog = Frog(5.0, 11.0, 0.0, 2.0)\n frog.bounce_off_banks(10, 10)\n assert frog.vy < 0 # Should reverse vertical velocity\n\n def test_no_bounce_within_bounds(self):\n frog = Frog(5.0, 5.0, 1.0, 1.0)\n original_vx, original_vy = frog.vx, frog.vy\n frog.bounce_off_banks(10, 10)\n assert frog.vx == original_vx\n assert frog.vy == original_vy\n\nclass TestFrogJumping:\n def test_normal_jump(self):\n frog = Frog(5.0, 5.0, 1.0, 1.0)\n expected_x = 5.0 + 1.0 * DT\n expected_y = 5.0 + 1.0 * DT\n frog.jump(10, 10)\n assert np.isclose(frog.x, expected_x)\n assert np.isclose(frog.y, expected_y)\n\n def test_jump_with_boundary_clipping(self):\n frog = Frog(9.9, 9.9, 10.0, 10.0)\n frog.jump(10, 10)\n assert frog.x == 10.0 # Should be clipped to pond width\n assert frog.y == 10.0 # Should be clipped to pond height\n\n def test_jump_with_negative_boundary_clipping(self):\n frog = Frog(0.1, 0.1, -10.0, -10.0)\n frog.jump(10, 10)\n assert frog.x == 0.0 # Should be clipped to minimum\n assert frog.y == 0.0 # Should be clipped to minimum\n\ndef test_croak():\n frog = Frog(0, 0, 0, 0)\n # Capture stdout\n captured_output = io.StringIO()\n sys.stdout = captured_output\n \n frog.croak(3)\n \n # Restore stdout\n sys.stdout = sys.__stdout__\n \n output = captured_output.getvalue().strip()\n expected_output = "croak\\ncroak\\ncroak"\n assert output == expected_output\n\ndef test_alternative_frog():\n alt_frog = AlternativeFrog()\n # Just verify it can be instantiated and method exists\n alt_frog.alternative_jump()\n\ndef test_standalone_jumping_function():\n captured_output = io.StringIO()\n sys.stdout = captured_output\n \n standalone_jumping_function()\n \n sys.stdout = sys.__stdout__\n \n output = captured_output.getvalue().strip()\n assert output == "I\'m a frog! Jump! Jump! Really!"\n', + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_015isrQXYKSQ4XYqECkXis3J", + }, + { + role: "assistant", + content: + "\n\nI've created a comprehensive test suite for the frog module. Here's what the tests cover:\n\n1. Basic Initialization\n- Tests that a Frog object is correctly initialized with given coordinates and velocities\n\n2. Bouncing Behavior (TestFrogBouncing class)\n- Tests bouncing off all four boundaries (left, right, top, bottom)\n- Tests that velocity is reversed appropriately when hitting boundaries\n- Tests that no bouncing occurs when the frog is within bounds\n\n3. Jumping Mechanics (TestFrogJumping class)\n- Tests normal jumping behavior with velocity and time step (DT)\n- Tests boundary clipping when jumping would exceed pond dimensions\n- Tests clipping at both maximum and minimum boundaries\n\n4. Croaking\n- Tests that the croak method outputs the correct number of \"croak\" messages\n\n5. Additional Components\n- Tests for AlternativeFrog class (basic instantiation and method existence)\n- Tests for the standalone_jumping_function\n\nThe tests use pytest and include proper handling of stdout capture for testing print statements. They also use numpy's isclose() for floating-point comparisons where appropriate.\n\nTo run these tests, you would need to have pytest installed and can run them using:\n```bash\npytest test_frog.py -v\n```\n\nThe tests are designed to be thorough while remaining readable and maintainable. Each test has a clear purpose and includes comments where necessary to explain the expected behavior.\n\nWould you like me to add any additional test cases or modify the existing ones?", + finish_reason: "stop", + }, + { + role: "user", + content: "Replace generated tests with well documented tests\n", + checkpoints: [ + { + workspace_folder: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation", + commit_hash: "b911227ed17022f6d828cb54a130f08c50273484", + }, + ], + }, + { + role: "assistant", + content: + "I'll create a new version of the tests with comprehensive documentation and clear test organization.", + tool_calls: [ + { + id: "toolu_019L5hGRf36DCj9q4BQfofBV", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", "replacement": "\\"\\"\\"\\nTests for the Frog simulation module.\\n\\nThis test suite verifies the behavior of a frog in a bounded pond environment.\\nThe tests cover movement mechanics, boundary interactions, and various frog actions.\\n\\nKey components tested:\\n- Frog initialization and state\\n- Boundary detection and bouncing behavior\\n- Movement and position updates\\n- Sound production (croaking)\\n\\"\\"\\"\\n\\nimport pytest\\nimport numpy as np\\nfrom frog import Frog, DT, AlternativeFrog, standalone_jumping_function\\nimport io\\nimport sys\\n\\n\\n@pytest.fixture\\ndef basic_frog():\\n \\"\\"\\"\\n Provides a basic frog instance for testing.\\n \\n Returns:\\n Frog: A frog instance initialized at (5, 5) with velocity (1, 1)\\n \\"\\"\\"\\n return Frog(5.0, 5.0, 1.0, 1.0)\\n\\n\\n@pytest.fixture\\ndef pond_dimensions():\\n \\"\\"\\"\\n Provides standard pond dimensions for testing.\\n \\n Returns:\\n tuple: (width, height) of the pond, both set to 10 units\\n \\"\\"\\"\\n return (10.0, 10.0)\\n\\n\\ndef test_frog_initialization():\\n \\"\\"\\"\\n Verify that a frog is correctly initialized with given parameters.\\n \\n Tests:\\n - Position coordinates (x, y)\\n - Velocity components (vx, vy)\\n \\"\\"\\"\\n x, y = 1.0, 2.0\\n vx, vy = 3.0, 4.0\\n frog = Frog(x, y, vx, vy)\\n \\n assert frog.x == x, f\\"Expected x-position to be {x}\\"\\n assert frog.y == y, f\\"Expected y-position to be {y}\\"\\n assert frog.vx == vx, f\\"Expected x-velocity to be {vx}\\"\\n assert frog.vy == vy, f\\"Expected y-velocity to be {vy}\\"\\n\\n\\nclass TestBoundaryBehavior:\\n \\"\\"\\"Tests for frog\'s interaction with pond boundaries.\\"\\"\\"\\n\\n @pytest.mark.parametrize(\\"test_case\\", [\\n # (starting_pos, starting_vel, expected_vel, description)\\n ((-1.0, 5.0), (-2.0, 0.0), (2.0, 0.0), \\"left boundary\\"),\\n ((11.0, 5.0), (2.0, 0.0), (-2.0, 0.0), \\"right boundary\\"),\\n ((5.0, -1.0), (0.0, -2.0), (0.0, 2.0), \\"bottom boundary\\"),\\n ((5.0, 11.0), (0.0, 2.0), (0.0, -2.0), \\"top boundary\\")\\n ])\\n def test_boundary_bouncing(self, test_case, pond_dimensions):\\n \\"\\"\\"\\n Test bouncing behavior at all pond boundaries.\\n \\n Verifies that velocity is correctly reversed when the frog hits a boundary.\\n \\n Args:\\n test_case: Tuple containing test parameters\\n pond_dimensions: Standard pond dimensions from fixture\\n \\"\\"\\"\\n pos, vel, expected, boundary = test_case\\n frog = Frog(pos[0], pos[1], vel[0], vel[1])\\n \\n frog.bounce_off_banks(*pond_dimensions)\\n \\n assert (frog.vx, frog.vy) == expected, \\\\\\n f\\"Incorrect velocity after bouncing off {boundary}\\"\\n\\n def test_no_bounce_within_bounds(self, basic_frog, pond_dimensions):\\n \\"\\"\\"\\n Verify that no velocity changes occur when frog is within pond boundaries.\\n \\"\\"\\"\\n original_vx, original_vy = basic_frog.vx, basic_frog.vy\\n \\n basic_frog.bounce_off_banks(*pond_dimensions)\\n \\n assert (basic_frog.vx, basic_frog.vy) == (original_vx, original_vy), \\\\\\n \\"Velocity should not change when frog is within bounds\\"\\n\\n\\nclass TestMovement:\\n \\"\\"\\"Tests for frog\'s movement mechanics.\\"\\"\\"\\n\\n def test_normal_jump(self, basic_frog, pond_dimensions):\\n \\"\\"\\"\\n Test standard jumping motion within pond boundaries.\\n \\n Verifies that position is updated correctly according to:\\n new_position = old_position + velocity * time_step\\n \\"\\"\\"\\n initial_x, initial_y = basic_frog.x, basic_frog.y\\n expected_x = initial_x + basic_frog.vx * DT\\n expected_y = initial_y + basic_frog.vy * DT\\n \\n basic_frog.jump(*pond_dimensions)\\n \\n assert np.isclose(basic_frog.x, expected_x), \\\\\\n \\"Incorrect x-position after jump\\"\\n assert np.isclose(basic_frog.y, expected_y), \\\\\\n \\"Incorrect y-position after jump\\"\\n\\n @pytest.mark.parametrize(\\"test_case\\", [\\n # (pos, vel, expected_pos, description)\\n ((9.9, 9.9), (10.0, 10.0), (10.0, 10.0), \\"upper bounds\\"),\\n ((0.1, 0.1), (-10.0, -10.0), (0.0, 0.0), \\"lower bounds\\")\\n ])\\n def test_boundary_clipping(self, test_case, pond_dimensions):\\n \\"\\"\\"\\n Test position clipping at pond boundaries.\\n \\n Verifies that frog\'s position is properly constrained within pond limits.\\n \\n Args:\\n test_case: Tuple containing test parameters\\n pond_dimensions: Standard pond dimensions from fixture\\n \\"\\"\\"\\n pos, vel, expected, bounds = test_case\\n frog = Frog(pos[0], pos[1], vel[0], vel[1])\\n \\n frog.jump(*pond_dimensions)\\n \\n assert (frog.x, frog.y) == expected, \\\\\\n f\\"Incorrect position clipping at {bounds}\\"\\n\\n\\ndef test_croak_output():\\n \\"\\"\\"\\n Test frog\'s croaking behavior.\\n \\n Verifies that:\\n - Correct number of croaks are produced\\n - Each croak is on a new line\\n - Output format matches expectations\\n \\"\\"\\"\\n frog = Frog(0, 0, 0, 0)\\n n_croaks = 3\\n captured_output = io.StringIO()\\n sys.stdout = captured_output\\n \\n try:\\n frog.croak(n_croaks)\\n output = captured_output.getvalue().strip()\\n expected_output = \\"croak\\\\n\\" * n_croaks\\n finally:\\n sys.stdout = sys.__stdout__\\n \\n assert output == expected_output.strip(), \\\\\\n f\\"Expected {n_croaks} croaks, got: {output}\\"\\n\\n\\ndef test_alternative_frog_existence():\\n \\"\\"\\"\\n Verify the existence and basic structure of AlternativeFrog class.\\n \\n This is a structural test to ensure the alternative implementation\\n maintains the expected interface.\\n \\"\\"\\"\\n alt_frog = AlternativeFrog()\\n assert hasattr(alt_frog, \'alternative_jump\'), \\\\\\n \\"AlternativeFrog missing alternative_jump method\\"\\n alt_frog.alternative_jump() # Should not raise any exceptions\\n\\n\\ndef test_standalone_jumping_function():\\n \\"\\"\\"\\n Test the standalone jumping function\'s output.\\n \\n Verifies the exact string output of the function, including punctuation\\n and spacing.\\n \\"\\"\\"\\n captured_output = io.StringIO()\\n sys.stdout = captured_output\\n \\n try:\\n standalone_jumping_function()\\n output = captured_output.getvalue().strip()\\n finally:\\n sys.stdout = sys.__stdout__\\n \\n expected = \\"I\'m a frog! Jump! Jump! Really!\\"\\n assert output == expected, \\\\\\n f\\"Unexpected output: {output}\\""}', + name: "replace_textdoc", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 1, + line2: 1, + lines_remove: "", + lines_add: + '"""\nTests for the Frog simulation module.\n\nThis test suite verifies the behavior of a frog in a bounded pond environment.\nThe tests cover movement mechanics, boundary interactions, and various frog actions.\n\nKey components tested:\n- Frog initialization and state\n- Boundary detection and bouncing behavior\n- Movement and position updates\n- Sound production (croaking)\n"""\n\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 4, + line2: 5, + lines_remove: "from unittest.mock import patch\n", + lines_add: "", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 8, + line2: 8, + lines_remove: "", + lines_add: + '\n@pytest.fixture\ndef basic_frog():\n """\n Provides a basic frog instance for testing.\n \n Returns:\n Frog: A frog instance initialized at (5, 5) with velocity (1, 1)\n """\n return Frog(5.0, 5.0, 1.0, 1.0)\n\n\n@pytest.fixture\ndef pond_dimensions():\n """\n Provides standard pond dimensions for testing.\n \n Returns:\n tuple: (width, height) of the pond, both set to 10 units\n """\n return (10.0, 10.0)\n\n\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 9, + line2: 14, + lines_remove: + " frog = Frog(1.0, 2.0, 3.0, 4.0)\n assert frog.x == 1.0\n assert frog.y == 2.0\n assert frog.vx == 3.0\n assert frog.vy == 4.0\n", + lines_add: + ' """\n Verify that a frog is correctly initialized with given parameters.\n \n Tests:\n - Position coordinates (x, y)\n - Velocity components (vx, vy)\n """\n x, y = 1.0, 2.0\n vx, vy = 3.0, 4.0\n frog = Frog(x, y, vx, vy)\n \n assert frog.x == x, f"Expected x-position to be {x}"\n assert frog.y == y, f"Expected y-position to be {y}"\n assert frog.vx == vx, f"Expected x-velocity to be {vx}"\n assert frog.vy == vy, f"Expected y-velocity to be {vy}"\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 15, + line2: 20, + lines_remove: + "class TestFrogBouncing:\n def test_bounce_off_left_bank(self):\n frog = Frog(-1.0, 5.0, -2.0, 0.0)\n frog.bounce_off_banks(10, 10)\n assert frog.vx > 0 # Should reverse horizontal velocity\n", + lines_add: "", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 21, + line2: 25, + lines_remove: + " def test_bounce_off_right_bank(self):\n frog = Frog(11.0, 5.0, 2.0, 0.0)\n frog.bounce_off_banks(10, 10)\n assert frog.vx < 0 # Should reverse horizontal velocity\n", + lines_add: + 'class TestBoundaryBehavior:\n """Tests for frog\'s interaction with pond boundaries."""\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 26, + line2: 30, + lines_remove: + " def test_bounce_off_bottom(self):\n frog = Frog(5.0, -1.0, 0.0, -2.0)\n frog.bounce_off_banks(10, 10)\n assert frog.vy > 0 # Should reverse vertical velocity\n", + lines_add: + ' @pytest.mark.parametrize("test_case", [\n # (starting_pos, starting_vel, expected_vel, description)\n ((-1.0, 5.0), (-2.0, 0.0), (2.0, 0.0), "left boundary"),\n ((11.0, 5.0), (2.0, 0.0), (-2.0, 0.0), "right boundary"),\n ((5.0, -1.0), (0.0, -2.0), (0.0, 2.0), "bottom boundary"),\n ((5.0, 11.0), (0.0, 2.0), (0.0, -2.0), "top boundary")\n ])\n def test_boundary_bouncing(self, test_case, pond_dimensions):\n """\n Test bouncing behavior at all pond boundaries.\n \n Verifies that velocity is correctly reversed when the frog hits a boundary.\n \n Args:\n test_case: Tuple containing test parameters\n pond_dimensions: Standard pond dimensions from fixture\n """\n pos, vel, expected, boundary = test_case\n frog = Frog(pos[0], pos[1], vel[0], vel[1])\n \n frog.bounce_off_banks(*pond_dimensions)\n \n assert (frog.vx, frog.vy) == expected, \\\n f"Incorrect velocity after bouncing off {boundary}"\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 31, + line2: 35, + lines_remove: + " def test_bounce_off_top(self):\n frog = Frog(5.0, 11.0, 0.0, 2.0)\n frog.bounce_off_banks(10, 10)\n assert frog.vy < 0 # Should reverse vertical velocity\n", + lines_add: + ' def test_no_bounce_within_bounds(self, basic_frog, pond_dimensions):\n """\n Verify that no velocity changes occur when frog is within pond boundaries.\n """\n original_vx, original_vy = basic_frog.vx, basic_frog.vy\n \n basic_frog.bounce_off_banks(*pond_dimensions)\n \n assert (basic_frog.vx, basic_frog.vy) == (original_vx, original_vy), \\\n "Velocity should not change when frog is within bounds"\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 36, + line2: 42, + lines_remove: + " def test_no_bounce_within_bounds(self):\n frog = Frog(5.0, 5.0, 1.0, 1.0)\n original_vx, original_vy = frog.vx, frog.vy\n frog.bounce_off_banks(10, 10)\n assert frog.vx == original_vx\n assert frog.vy == original_vy\n", + lines_add: "", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 43, + line2: 51, + lines_remove: + "class TestFrogJumping:\n def test_normal_jump(self):\n frog = Frog(5.0, 5.0, 1.0, 1.0)\n expected_x = 5.0 + 1.0 * DT\n expected_y = 5.0 + 1.0 * DT\n frog.jump(10, 10)\n assert np.isclose(frog.x, expected_x)\n assert np.isclose(frog.y, expected_y)\n", + lines_add: + 'class TestMovement:\n """Tests for frog\'s movement mechanics."""\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 52, + line2: 57, + lines_remove: + " def test_jump_with_boundary_clipping(self):\n frog = Frog(9.9, 9.9, 10.0, 10.0)\n frog.jump(10, 10)\n assert frog.x == 10.0 # Should be clipped to pond width\n assert frog.y == 10.0 # Should be clipped to pond height\n", + lines_add: + ' def test_normal_jump(self, basic_frog, pond_dimensions):\n """\n Test standard jumping motion within pond boundaries.\n \n Verifies that position is updated correctly according to:\n new_position = old_position + velocity * time_step\n """\n initial_x, initial_y = basic_frog.x, basic_frog.y\n expected_x = initial_x + basic_frog.vx * DT\n expected_y = initial_y + basic_frog.vy * DT\n \n basic_frog.jump(*pond_dimensions)\n \n assert np.isclose(basic_frog.x, expected_x), \\\n "Incorrect x-position after jump"\n assert np.isclose(basic_frog.y, expected_y), \\\n "Incorrect y-position after jump"\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 58, + line2: 63, + lines_remove: + " def test_jump_with_negative_boundary_clipping(self):\n frog = Frog(0.1, 0.1, -10.0, -10.0)\n frog.jump(10, 10)\n assert frog.x == 0.0 # Should be clipped to minimum\n assert frog.y == 0.0 # Should be clipped to minimum\n", + lines_add: + ' @pytest.mark.parametrize("test_case", [\n # (pos, vel, expected_pos, description)\n ((9.9, 9.9), (10.0, 10.0), (10.0, 10.0), "upper bounds"),\n ((0.1, 0.1), (-10.0, -10.0), (0.0, 0.0), "lower bounds")\n ])\n def test_boundary_clipping(self, test_case, pond_dimensions):\n """\n Test position clipping at pond boundaries.\n \n Verifies that frog\'s position is properly constrained within pond limits.\n \n Args:\n test_case: Tuple containing test parameters\n pond_dimensions: Standard pond dimensions from fixture\n """\n pos, vel, expected, bounds = test_case\n frog = Frog(pos[0], pos[1], vel[0], vel[1])\n \n frog.jump(*pond_dimensions)\n \n assert (frog.x, frog.y) == expected, \\\n f"Incorrect position clipping at {bounds}"\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 64, + line2: 65, + lines_remove: "def test_croak():\n", + lines_add: + '\ndef test_croak_output():\n """\n Test frog\'s croaking behavior.\n \n Verifies that:\n - Correct number of croaks are produced\n - Each croak is on a new line\n - Output format matches expectations\n """\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 66, + line2: 67, + lines_remove: " # Capture stdout\n", + lines_add: " n_croaks = 3\n", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 70, + line2: 71, + lines_remove: " frog.croak(3)\n", + lines_add: + ' try:\n frog.croak(n_croaks)\n output = captured_output.getvalue().strip()\n expected_output = "croak\\n" * n_croaks\n finally:\n sys.stdout = sys.__stdout__\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 72, + line2: 78, + lines_remove: + ' # Restore stdout\n sys.stdout = sys.__stdout__\n \n output = captured_output.getvalue().strip()\n expected_output = "croak\\ncroak\\ncroak"\n assert output == expected_output\n', + lines_add: + ' assert output == expected_output.strip(), \\\n f"Expected {n_croaks} croaks, got: {output}"\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 79, + line2: 80, + lines_remove: "def test_alternative_frog():\n", + lines_add: + '\ndef test_alternative_frog_existence():\n """\n Verify the existence and basic structure of AlternativeFrog class.\n \n This is a structural test to ensure the alternative implementation\n maintains the expected interface.\n """\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 81, + line2: 83, + lines_remove: + " # Just verify it can be instantiated and method exists\n alt_frog.alternative_jump()\n", + lines_add: + " assert hasattr(alt_frog, 'alternative_jump'), \\\n \"AlternativeFrog missing alternative_jump method\"\n alt_frog.alternative_jump() # Should not raise any exceptions\n", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 84, + line2: 84, + lines_remove: "", + lines_add: "\n", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 85, + line2: 85, + lines_remove: "", + lines_add: + ' """\n Test the standalone jumping function\'s output.\n \n Verifies the exact string output of the function, including punctuation\n and spacing.\n """\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 88, + line2: 89, + lines_remove: " standalone_jumping_function()\n", + lines_add: + " try:\n standalone_jumping_function()\n output = captured_output.getvalue().strip()\n finally:\n sys.stdout = sys.__stdout__\n", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 90, + line2: 94, + lines_remove: + ' sys.stdout = sys.__stdout__\n \n output = captured_output.getvalue().strip()\n assert output == "I\'m a frog! Jump! Jump! Really!"\n', + lines_add: + ' expected = "I\'m a frog! Jump! Jump! Really!"\n assert output == expected, \\\n f"Unexpected output: {output}"\n', + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_019L5hGRf36DCj9q4BQfofBV", + }, + { + role: "assistant", + content: + "\n\nI've completely rewritten the tests with comprehensive documentation and better organization. Here are the key improvements:\n\n1. Added Module-Level Documentation\n- Clear description of what the test suite covers\n- Key components being tested\n- Overall structure\n\n2. Added Fixtures\n- `basic_frog`: Provides a standard frog instance for testing\n- `pond_dimensions`: Provides standard pond dimensions\n- Both fixtures are well-documented with docstrings\n\n3. Organized Tests into Logical Classes\n- `TestBoundaryBehavior`: Tests for boundary interactions\n- `TestMovement`: Tests for movement mechanics\n\n4. Improved Test Documentation\n- Each test function has a detailed docstring\n- Clear description of what is being tested\n- Expected behavior and conditions\n- Any special considerations\n\n5. Better Test Structure\n- Used parametrized tests where appropriate to reduce code duplication\n- Clear separation of setup, action, and assertion phases\n- Meaningful assertion messages\n\n6. Improved Error Handling\n- Proper cleanup of system resources (stdout capture)\n- Better error messages with context\n- Use of try/finally blocks where appropriate\n\n7. Better Test Names\n- More descriptive test names that indicate what's being tested\n- Consistent naming convention\n\nKey Features:\n1. Boundary Testing\n- Comprehensive tests for all pond boundaries\n- Tests for both position and velocity changes\n- Clear error messages for boundary violations\n\n2. Movement Testing\n- Precise testing of movement calculations\n- Boundary clipping verification\n- Multiple test cases for different scenarios\n\n3. Output Testing\n- Proper capture and verification of stdout\n- Exact string matching for croaks and messages\n- Resource cleanup after stdout capture\n\n4. Structure Testing\n- Verification of alternative implementations\n- Interface consistency checks\n- Existence of required methods\n\nThe tests are now more maintainable, readable, and provide better feedback when failures occur. Each test has a clear purpose and includes proper documentation about what it's testing and why.\n\nWould you like me to explain any particular part of the test suite in more detail?", + finish_reason: "stop", + }, + { + role: "user", + content: + "Great! Update the document and remove all docstrings one by one", + checkpoints: [ + { + workspace_folder: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation", + commit_hash: "0609d3823dd9e8ac84674cefb5dcf5d0b691e890", + }, + ], + }, + { + role: "assistant", + content: + "I'll help you remove the docstrings one by one from the test file while preserving the functionality.", + tool_calls: [ + { + id: "toolu_01KmJrYK6xn2XqGLXpFA5Tgp", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", "old_str": "\\"\\"\\"\\nTests for the Frog simulation module.\\n\\nThis test suite verifies the behavior of a frog in a bounded pond environment.\\nThe tests cover movement mechanics, boundary interactions, and various frog actions.\\n\\nKey components tested:\\n- Frog initialization and state\\n- Boundary detection and bouncing behavior\\n- Movement and position updates\\n- Sound production (croaking)\\n\\"\\"\\"", "replacement": "", "multiple": false}', + name: "update_textdoc", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 1, + line2: 3, + lines_remove: '"""\nTests for the Frog simulation module.\n', + lines_add: "", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 4, + line2: 13, + lines_remove: + 'This test suite verifies the behavior of a frog in a bounded pond environment.\nThe tests cover movement mechanics, boundary interactions, and various frog actions.\n\nKey components tested:\n- Frog initialization and state\n- Boundary detection and bouncing behavior\n- Movement and position updates\n- Sound production (croaking)\n"""\n', + lines_add: "", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_01KmJrYK6xn2XqGLXpFA5Tgp", + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "toolu_01Smt5a61QbmbmZHpbNh7BKK", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", "old_str": " \\"\\"\\"\\n Provides a basic frog instance for testing.\\n \\n Returns:\\n Frog: A frog instance initialized at (5, 5) with velocity (1, 1)\\n \\"\\"\\"", "replacement": "", "multiple": false}', + name: "update_textdoc", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 12, + line2: 18, + lines_remove: + ' """\n Provides a basic frog instance for testing.\n \n Returns:\n Frog: A frog instance initialized at (5, 5) with velocity (1, 1)\n """\n', + lines_add: "\n", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_01Smt5a61QbmbmZHpbNh7BKK", + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "toolu_0168Gio2kBMYhzKe6bdxErkb", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", "old_str": " \\"\\"\\"\\n Provides standard pond dimensions for testing.\\n \\n Returns:\\n tuple: (width, height) of the pond, both set to 10 units\\n \\"\\"\\"", "replacement": "", "multiple": false}', + name: "update_textdoc", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 18, + line2: 24, + lines_remove: + ' """\n Provides standard pond dimensions for testing.\n \n Returns:\n tuple: (width, height) of the pond, both set to 10 units\n """\n', + lines_add: "\n", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_0168Gio2kBMYhzKe6bdxErkb", + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "toolu_016ChnUcGQGrjfbx2dSzmmeR", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", "old_str": " \\"\\"\\"\\n Verify that a frog is correctly initialized with given parameters.\\n \\n Tests:\\n - Position coordinates (x, y)\\n - Velocity components (vx, vy)\\n \\"\\"\\"", "replacement": "", "multiple": false}', + name: "update_textdoc", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 23, + line2: 30, + lines_remove: + ' """\n Verify that a frog is correctly initialized with given parameters.\n \n Tests:\n - Position coordinates (x, y)\n - Velocity components (vx, vy)\n """\n', + lines_add: "\n", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_016ChnUcGQGrjfbx2dSzmmeR", + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "toolu_01Fzi693dL7aKwE9gLFetqiN", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", "old_str": " \\"\\"\\"Tests for frog\'s interaction with pond boundaries.\\"\\"\\"", "replacement": "", "multiple": false}', + name: "update_textdoc", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 35, + line2: 36, + lines_remove: + ' """Tests for frog\'s interaction with pond boundaries."""\n', + lines_add: "\n", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_01Fzi693dL7aKwE9gLFetqiN", + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "toolu_01Xtt2L5ctWViSFce6JcNR2Z", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", "old_str": " \\"\\"\\"\\n Test bouncing behavior at all pond boundaries.\\n \\n Verifies that velocity is correctly reversed when the frog hits a boundary.\\n \\n Args:\\n test_case: Tuple containing test parameters\\n pond_dimensions: Standard pond dimensions from fixture\\n \\"\\"\\"", "replacement": "", "multiple": false}', + name: "update_textdoc", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 45, + line2: 54, + lines_remove: + ' """\n Test bouncing behavior at all pond boundaries.\n \n Verifies that velocity is correctly reversed when the frog hits a boundary.\n \n Args:\n test_case: Tuple containing test parameters\n pond_dimensions: Standard pond dimensions from fixture\n """\n', + lines_add: "\n", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_01Xtt2L5ctWViSFce6JcNR2Z", + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "toolu_01WoEFVoJi9wSp2XgTx5PCUo", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", "old_str": " \\"\\"\\"\\n Verify that no velocity changes occur when frog is within pond boundaries.\\n \\"\\"\\"", "replacement": "", "multiple": false}', + name: "update_textdoc", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 55, + line2: 58, + lines_remove: + ' """\n Verify that no velocity changes occur when frog is within pond boundaries.\n """\n', + lines_add: "\n", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_01WoEFVoJi9wSp2XgTx5PCUo", + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "toolu_017YWfgyTtHRLZjm3hrfCTBK", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", "old_str": " \\"\\"\\"Tests for frog\'s movement mechanics.\\"\\"\\"", "replacement": "", "multiple": false}', + name: "update_textdoc", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 65, + line2: 66, + lines_remove: ' """Tests for frog\'s movement mechanics."""\n', + lines_add: "\n", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_017YWfgyTtHRLZjm3hrfCTBK", + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "toolu_01NsGmmfJfDR5UhmBE4N8kD4", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", "old_str": " \\"\\"\\"\\n Test standard jumping motion within pond boundaries.\\n \\n Verifies that position is updated correctly according to:\\n new_position = old_position + velocity * time_step\\n \\"\\"\\"", "replacement": "", "multiple": false}', + name: "update_textdoc", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 68, + line2: 74, + lines_remove: + ' """\n Test standard jumping motion within pond boundaries.\n \n Verifies that position is updated correctly according to:\n new_position = old_position + velocity * time_step\n """\n', + lines_add: "\n", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_01NsGmmfJfDR5UhmBE4N8kD4", + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "toolu_01EXrnY36zeat9gdMWAaiPDf", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", "old_str": " \\"\\"\\"\\n Test position clipping at pond boundaries.\\n \\n Verifies that frog\'s position is properly constrained within pond limits.\\n \\n Args:\\n test_case: Tuple containing test parameters\\n pond_dimensions: Standard pond dimensions from fixture\\n \\"\\"\\"", "replacement": "", "multiple": false}', + name: "update_textdoc", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 86, + line2: 95, + lines_remove: + ' """\n Test position clipping at pond boundaries.\n \n Verifies that frog\'s position is properly constrained within pond limits.\n \n Args:\n test_case: Tuple containing test parameters\n pond_dimensions: Standard pond dimensions from fixture\n """\n', + lines_add: "\n", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_01EXrnY36zeat9gdMWAaiPDf", + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "toolu_01KqLh5gkGZEMkXzRjdQWcev", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", "old_str": " \\"\\"\\"\\n Test frog\'s croaking behavior.\\n \\n Verifies that:\\n - Correct number of croaks are produced\\n - Each croak is on a new line\\n - Output format matches expectations\\n \\"\\"\\"", "replacement": "", "multiple": false}', + name: "update_textdoc", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 97, + line2: 105, + lines_remove: + ' """\n Test frog\'s croaking behavior.\n \n Verifies that:\n - Correct number of croaks are produced\n - Each croak is on a new line\n - Output format matches expectations\n """\n', + lines_add: "\n", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_01KqLh5gkGZEMkXzRjdQWcev", + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "toolu_01GtAM91bdJY74XQRBhpqh8K", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", "old_str": " \\"\\"\\"\\n Verify the existence and basic structure of AlternativeFrog class.\\n \\n This is a structural test to ensure the alternative implementation\\n maintains the expected interface.\\n \\"\\"\\"", "replacement": "", "multiple": false}', + name: "update_textdoc", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 115, + line2: 121, + lines_remove: + ' """\n Verify the existence and basic structure of AlternativeFrog class.\n \n This is a structural test to ensure the alternative implementation\n maintains the expected interface.\n """\n', + lines_add: "\n", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_01GtAM91bdJY74XQRBhpqh8K", + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "toolu_01FrfBzW3RjxL3okQUqRiyWf", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", "old_str": " \\"\\"\\"\\n Test the standalone jumping function\'s output.\\n \\n Verifies the exact string output of the function, including punctuation\\n and spacing.\\n \\"\\"\\"", "replacement": "", "multiple": false}', + name: "update_textdoc", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 123, + line2: 129, + lines_remove: + ' """\n Test the standalone jumping function\'s output.\n \n Verifies the exact string output of the function, including punctuation\n and spacing.\n """\n', + lines_add: "\n", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_01FrfBzW3RjxL3okQUqRiyWf", + }, + { + role: "assistant", + content: + "\n\nNow all docstrings have been removed while preserving the test functionality. Here's a summary of what the test file contains:\n\n1. Fixtures:\n- basic_frog: Creates a standard frog at (5, 5) with velocity (1, 1)\n- pond_dimensions: Provides standard 10x10 pond dimensions\n\n2. Test Classes:\n- TestBoundaryBehavior: Tests boundary interactions with parametrized tests\n- TestMovement: Tests movement mechanics and position updates\n\n3. Standalone Tests:\n- test_frog_initialization: Basic initialization test\n- test_croak_output: Tests croaking functionality\n- test_alternative_frog_existence: Tests AlternativeFrog interface\n- test_standalone_jumping_function: Tests standalone function\n\nThe tests remain well-structured and maintain good coverage of the frog module's functionality, just without the documentation strings. Would you like me to show you the final version of the file or make any additional changes?", + finish_reason: "stop", + }, + { + role: "user", + content: "Great! Now remove all comments using regex\n", + checkpoints: [ + { + workspace_folder: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation", + commit_hash: "f1234facf0fd15104484f73c8e4cebc929a5d4d5", + }, + ], + }, + { + role: "assistant", + content: + "I'll use update_textdoc_regex to remove all comments from the test file.", + tool_calls: [ + { + id: "toolu_01LHPjgzJ8SEuczfm6Av7qfv", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", "pattern": "\\\\s*#.*$", "replacement": "", "multiple": true}', + name: "update_textdoc_regex", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "tool", + content: { + tool_call_id: "toolu_01LHPjgzJ8SEuczfm6Av7qfv", + content: + 'No replacement was performed, `pattern` \n```\n\\s*#.*$\n```\ndid not appear verbatim in "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py". Consider checking the file content using `cat()`', + }, + }, + { + role: "assistant", + content: + "\n\nLet me check the file content to see the exact format of comments:", + tool_calls: [ + { + id: "toolu_019iakkKqUjKP73EmEgVhCkZ", + function: { + arguments: + '{"paths": "tests/emergency_frog_situation/test_frog.py", "skeleton": false}', + name: "cat", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "tool", + content: { + tool_call_id: "toolu_019iakkKqUjKP73EmEgVhCkZ", + content: + "Paths found:\n/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py\n", + }, + }, + { + role: "context_file", + content: [ + { + file_name: "test_frog.py", + file_content: + '\n\nimport pytest\nimport numpy as np\nfrom frog import Frog, DT, AlternativeFrog, standalone_jumping_function\nimport io\nimport sys\n\n\n@pytest.fixture\ndef basic_frog():\n\n return Frog(5.0, 5.0, 1.0, 1.0)\n\n\n@pytest.fixture\ndef pond_dimensions():\n\n return (10.0, 10.0)\n\n\ndef test_frog_initialization():\n\n x, y = 1.0, 2.0\n vx, vy = 3.0, 4.0\n frog = Frog(x, y, vx, vy)\n \n assert frog.x == x, f"Expected x-position to be {x}"\n assert frog.y == y, f"Expected y-position to be {y}"\n assert frog.vx == vx, f"Expected x-velocity to be {vx}"\n assert frog.vy == vy, f"Expected y-velocity to be {vy}"\n\n\nclass TestBoundaryBehavior:\n\n\n @pytest.mark.parametrize("test_case", [\n # (starting_pos, starting_vel, expected_vel, description)\n ((-1.0, 5.0), (-2.0, 0.0), (2.0, 0.0), "left boundary"),\n ((11.0, 5.0), (2.0, 0.0), (-2.0, 0.0), "right boundary"),\n ((5.0, -1.0), (0.0, -2.0), (0.0, 2.0), "bottom boundary"),\n ((5.0, 11.0), (0.0, 2.0), (0.0, -2.0), "top boundary")\n ])\n def test_boundary_bouncing(self, test_case, pond_dimensions):\n\n pos, vel, expected, boundary = test_case\n frog = Frog(pos[0], pos[1], vel[0], vel[1])\n \n frog.bounce_off_banks(*pond_dimensions)\n \n assert (frog.vx, frog.vy) == expected, \\\n f"Incorrect velocity after bouncing off {boundary}"\n\n def test_no_bounce_within_bounds(self, basic_frog, pond_dimensions):\n\n original_vx, original_vy = basic_frog.vx, basic_frog.vy\n \n basic_frog.bounce_off_banks(*pond_dimensions)\n \n assert (basic_frog.vx, basic_frog.vy) == (original_vx, original_vy), \\\n "Velocity should not change when frog is within bounds"\n\n\nclass TestMovement:\n\n\n def test_normal_jump(self, basic_frog, pond_dimensions):\n\n initial_x, initial_y = basic_frog.x, basic_frog.y\n expected_x = initial_x + basic_frog.vx * DT\n expected_y = initial_y + basic_frog.vy * DT\n \n basic_frog.jump(*pond_dimensions)\n \n assert np.isclose(basic_frog.x, expected_x), \\\n "Incorrect x-position after jump"\n assert np.isclose(basic_frog.y, expected_y), \\\n "Incorrect y-position after jump"\n\n @pytest.mark.parametrize("test_case", [\n # (pos, vel, expected_pos, description)\n ((9.9, 9.9), (10.0, 10.0), (10.0, 10.0), "upper bounds"),\n ((0.1, 0.1), (-10.0, -10.0), (0.0, 0.0), "lower bounds")\n ])\n def test_boundary_clipping(self, test_case, pond_dimensions):\n\n pos, vel, expected, bounds = test_case\n frog = Frog(pos[0], pos[1], vel[0], vel[1])\n \n frog.jump(*pond_dimensions)\n \n assert (frog.x, frog.y) == expected, \\\n f"Incorrect position clipping at {bounds}"\n\n\ndef test_croak_output():\n\n frog = Frog(0, 0, 0, 0)\n n_croaks = 3\n captured_output = io.StringIO()\n sys.stdout = captured_output\n \n try:\n frog.croak(n_croaks)\n output = captured_output.getvalue().strip()\n expected_output = "croak\\n" * n_croaks\n finally:\n sys.stdout = sys.__stdout__\n \n assert output == expected_output.strip(), \\\n f"Expected {n_croaks} croaks, got: {output}"\n\n\ndef test_alternative_frog_existence():\n\n alt_frog = AlternativeFrog()\n assert hasattr(alt_frog, \'alternative_jump\'), \\\n "AlternativeFrog missing alternative_jump method"\n alt_frog.alternative_jump() # Should not raise any exceptions\n\n\ndef test_standalone_jumping_function():\n\n captured_output = io.StringIO()\n sys.stdout = captured_output\n \n try:\n standalone_jumping_function()\n output = captured_output.getvalue().strip()\n finally:\n sys.stdout = sys.__stdout__\n \n expected = "I\'m a frog! Jump! Jump! Really!"\n assert output == expected, \\\n f"Unexpected output: {output}"\n', + line1: 1, + line2: 134, + }, + ], + }, + { + role: "assistant", + content: + "I see the comments now. Let me remove them using regex that matches both inline comments and comment lines:", + tool_calls: [ + { + id: "toolu_01PXY5nTtbMuiA4JAjN5TMtX", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", "pattern": "\\\\s*#.*(\\\\n|$)", "replacement": "\\\\n", "multiple": true}', + name: "update_textdoc_regex", + }, + type: "function", + index: 0, + }, + ], + finish_reason: "stop", + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 37, + line2: 40, + lines_remove: + ' @pytest.mark.parametrize("test_case", [\n # (starting_pos, starting_vel, expected_vel, description)\n ((-1.0, 5.0), (-2.0, 0.0), (2.0, 0.0), "left boundary"),\n', + lines_add: + ' @pytest.mark.parametrize("test_case", [\\n ((-1.0, 5.0), (-2.0, 0.0), (2.0, 0.0), "left boundary"),\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 80, + line2: 83, + lines_remove: + ' @pytest.mark.parametrize("test_case", [\n # (pos, vel, expected_pos, description)\n ((9.9, 9.9), (10.0, 10.0), (10.0, 10.0), "upper bounds"),\n', + lines_add: + ' @pytest.mark.parametrize("test_case", [\\n ((9.9, 9.9), (10.0, 10.0), (10.0, 10.0), "upper bounds"),\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/test_frog.py", + file_action: "edit", + line1: 119, + line2: 121, + lines_remove: + " alt_frog.alternative_jump() # Should not raise any exceptions\n\n", + lines_add: " alt_frog.alternative_jump()\\n\n", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_01PXY5nTtbMuiA4JAjN5TMtX", + }, + { + role: "assistant", + content: + "\n\nGreat! All comments have been removed from the test file. The functionality remains exactly the same, but now without any inline comments or comment lines. The test file is now cleaner and more concise.\n\nWould you like me to show you the final version of the file or make any other changes?", + finish_reason: "stop", + }, + ], + title: "Frog Tests Generation", + model: "", + last_user_message_id: "e6304800-f080-425c-b1e3-c1f2ce267792", + tool_use: "agent", + mode: "AGENT", + read: true, + isTitleGenerated: true, + createdAt: "2025-02-14T06:47:26.640Z", + updatedAt: "2025-02-14T06:47:26.640Z", +}; From 6d1a7655fca256f1b3c715196784ef9e6cb84461 Mon Sep 17 00:00:00 2001 From: Marc McIntosh Date: Tue, 18 Feb 2025 14:42:07 +0100 Subject: [PATCH 3/4] chore(chat): update repository url in package.jsson --- refact-agent/gui/package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/refact-agent/gui/package.json b/refact-agent/gui/package.json index 9ef5f0c82..d73ecf5a8 100644 --- a/refact-agent/gui/package.json +++ b/refact-agent/gui/package.json @@ -23,10 +23,11 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/smallcloudai/refact-chat-js.git" + "url": "git+https://github.com/smallcloudai/refact.git", + "directory": "refact-agent/gui" }, "bugs": { - "url": "https://github.com/smallcloudai/refact-chat-js/issues" + "url": "https://github.com/smallcloudai/refact/issues" }, "scripts": { "dev": "vite", From fcced505a3ba5974432c3f034a1e303fe21a8e74 Mon Sep 17 00:00:00 2001 From: Marc McIntosh Date: Tue, 18 Feb 2025 18:18:41 +0100 Subject: [PATCH 4/4] ui: back work on adding an in memory router. --- refact-agent/gui/package-lock.json | 46 +++ refact-agent/gui/package.json | 1 + refact-agent/gui/public/mockServiceWorker.js | 182 ++++----- refact-agent/gui/src/components/Chat/Chat.tsx | 10 +- .../gui/src/components/ChatForm/ChatForm.tsx | 17 +- .../components/ChatHistory/ChatHistory.tsx | 11 +- .../components/ChatHistory/HistoryItem.tsx | 32 +- .../Layout.module.css} | 4 +- .../PageWrapper.tsx => Layout/Layout.tsx} | 25 +- .../components/Layout/LayoutWithTopbar.tsx | 13 + .../gui/src/components/Layout/index.tsx | 2 + .../gui/src/components/PageWrapper/index.tsx | 2 - .../gui/src/components/Toolbar/Toolbar.tsx | 85 ++-- refact-agent/gui/src/features/App.tsx | 368 ++++++++++++------ refact-agent/gui/src/features/Chat/Chat.tsx | 53 ++- .../gui/src/features/Chat/Thread/selectors.ts | 22 ++ .../gui/src/features/FIM/FIMDebug.tsx | 8 +- .../features/Integrations/Integrations.tsx | 7 +- .../src/features/Statistics/Statistics.tsx | 6 +- refact-agent/gui/src/utils/test-utils.tsx | 17 +- 20 files changed, 578 insertions(+), 333 deletions(-) rename refact-agent/gui/src/components/{PageWrapper/PageWrapper.module.css => Layout/Layout.module.css} (85%) rename refact-agent/gui/src/components/{PageWrapper/PageWrapper.tsx => Layout/Layout.tsx} (57%) create mode 100644 refact-agent/gui/src/components/Layout/LayoutWithTopbar.tsx create mode 100644 refact-agent/gui/src/components/Layout/index.tsx delete mode 100644 refact-agent/gui/src/components/PageWrapper/index.tsx diff --git a/refact-agent/gui/package-lock.json b/refact-agent/gui/package-lock.json index cd7e6de0d..c490d0e94 100644 --- a/refact-agent/gui/package-lock.json +++ b/refact-agent/gui/package-lock.json @@ -71,6 +71,7 @@ "react-dom": "^18.2.0", "react-dropzone": "^14.2.10", "react-markdown": "^9.0.1", + "react-router": "^7.1.5", "react-syntax-highlighter": "^15.5.0", "redux-persist": "^6.0.0", "rehype-katex": "^7.0.0", @@ -20211,6 +20212,39 @@ } } }, + "node_modules/react-router": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.5.tgz", + "integrity": "sha512-8BUF+hZEU4/z/JD201yK6S+UYhsf58bzYIDq2NS1iGpwxSXDu7F+DeGSkIXMFBuHZB21FSiCzEcUb18cQNdRkA==", + "dev": true, + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -21209,6 +21243,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -22335,6 +22375,12 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "dev": true + }, "node_modules/tween-functions": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz", diff --git a/refact-agent/gui/package.json b/refact-agent/gui/package.json index d73ecf5a8..6ebbfc68b 100644 --- a/refact-agent/gui/package.json +++ b/refact-agent/gui/package.json @@ -110,6 +110,7 @@ "react-dom": "^18.2.0", "react-dropzone": "^14.2.10", "react-markdown": "^9.0.1", + "react-router": "^7.1.5", "react-syntax-highlighter": "^15.5.0", "redux-persist": "^6.0.0", "rehype-katex": "^7.0.0", diff --git a/refact-agent/gui/public/mockServiceWorker.js b/refact-agent/gui/public/mockServiceWorker.js index 3e31753cc..cbd28e53a 100644 --- a/refact-agent/gui/public/mockServiceWorker.js +++ b/refact-agent/gui/public/mockServiceWorker.js @@ -8,128 +8,128 @@ * - Please do NOT serve this file on production. */ -const PACKAGE_VERSION = "2.3.4"; -const INTEGRITY_CHECKSUM = "26357c79639bfa20d64c0efca2a87423"; -const IS_MOCKED_RESPONSE = Symbol("isMockedResponse"); -const activeClientIds = new Set(); +const PACKAGE_VERSION = '2.3.4' +const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() -self.addEventListener("install", function () { - self.skipWaiting(); -}); +self.addEventListener('install', function () { + self.skipWaiting() +}) -self.addEventListener("activate", function (event) { - event.waitUntil(self.clients.claim()); -}); +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) -self.addEventListener("message", async function (event) { - const clientId = event.source.id; +self.addEventListener('message', async function (event) { + const clientId = event.source.id if (!clientId || !self.clients) { - return; + return } - const client = await self.clients.get(clientId); + const client = await self.clients.get(clientId) if (!client) { - return; + return } const allClients = await self.clients.matchAll({ - type: "window", - }); + type: 'window', + }) switch (event.data) { - case "KEEPALIVE_REQUEST": { + case 'KEEPALIVE_REQUEST': { sendToClient(client, { - type: "KEEPALIVE_RESPONSE", - }); - break; + type: 'KEEPALIVE_RESPONSE', + }) + break } - case "INTEGRITY_CHECK_REQUEST": { + case 'INTEGRITY_CHECK_REQUEST': { sendToClient(client, { - type: "INTEGRITY_CHECK_RESPONSE", + type: 'INTEGRITY_CHECK_RESPONSE', payload: { packageVersion: PACKAGE_VERSION, checksum: INTEGRITY_CHECKSUM, }, - }); - break; + }) + break } - case "MOCK_ACTIVATE": { - activeClientIds.add(clientId); + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) sendToClient(client, { - type: "MOCKING_ENABLED", + type: 'MOCKING_ENABLED', payload: true, - }); - break; + }) + break } - case "MOCK_DEACTIVATE": { - activeClientIds.delete(clientId); - break; + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break } - case "CLIENT_CLOSED": { - activeClientIds.delete(clientId); + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) const remainingClients = allClients.filter((client) => { - return client.id !== clientId; - }); + return client.id !== clientId + }) // Unregister itself when there are no more clients if (remainingClients.length === 0) { - self.registration.unregister(); + self.registration.unregister() } - break; + break } } -}); +}) -self.addEventListener("fetch", function (event) { - const { request } = event; +self.addEventListener('fetch', function (event) { + const { request } = event // Bypass navigation requests. - if (request.mode === "navigate") { - return; + if (request.mode === 'navigate') { + return } // Opening the DevTools triggers the "only-if-cached" request // that cannot be handled by the worker. Bypass such requests. - if (request.cache === "only-if-cached" && request.mode !== "same-origin") { - return; + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return } // Bypass all requests when there are no active clients. // Prevents the self-unregistered worked from handling requests // after it's been deleted (still remains active until the next reload). if (activeClientIds.size === 0) { - return; + return } // Generate unique request ID. - const requestId = crypto.randomUUID(); - event.respondWith(handleRequest(event, requestId)); -}); + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) async function handleRequest(event, requestId) { - const client = await resolveMainClient(event); - const response = await getResponse(event, client, requestId); + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) // Send back the response clone for the "response:*" life-cycle events. // Ensure MSW is active and ready to handle the message, otherwise // this message will pend indefinitely. if (client && activeClientIds.has(client.id)) { - (async function () { - const responseClone = response.clone(); + ;(async function () { + const responseClone = response.clone() sendToClient( client, { - type: "RESPONSE", + type: 'RESPONSE', payload: { requestId, isMockedResponse: IS_MOCKED_RESPONSE in response, @@ -141,11 +141,11 @@ async function handleRequest(event, requestId) { }, }, [responseClone.body], - ); - })(); + ) + })() } - return response; + return response } // Resolve the main client for the given event. @@ -153,49 +153,49 @@ async function handleRequest(event, requestId) { // that registered the worker. It's with the latter the worker should // communicate with during the response resolving phase. async function resolveMainClient(event) { - const client = await self.clients.get(event.clientId); + const client = await self.clients.get(event.clientId) - if (client?.frameType === "top-level") { - return client; + if (client?.frameType === 'top-level') { + return client } const allClients = await self.clients.matchAll({ - type: "window", - }); + type: 'window', + }) return allClients .filter((client) => { // Get only those clients that are currently visible. - return client.visibilityState === "visible"; + return client.visibilityState === 'visible' }) .find((client) => { // Find the client ID that's recorded in the // set of clients that have registered the worker. - return activeClientIds.has(client.id); - }); + return activeClientIds.has(client.id) + }) } async function getResponse(event, client, requestId) { - const { request } = event; + const { request } = event // Clone the request because it might've been already used // (i.e. its body has been read and sent to the client). - const requestClone = request.clone(); + const requestClone = request.clone() function passthrough() { - const headers = Object.fromEntries(requestClone.headers.entries()); + const headers = Object.fromEntries(requestClone.headers.entries()) // Remove internal MSW request header so the passthrough request // complies with any potential CORS preflight checks on the server. // Some servers forbid unknown request headers. - delete headers["x-msw-intention"]; + delete headers['x-msw-intention'] - return fetch(requestClone, { headers }); + return fetch(requestClone, { headers }) } // Bypass mocking when the client is not active. if (!client) { - return passthrough(); + return passthrough() } // Bypass initial page load requests (i.e. static assets). @@ -203,15 +203,15 @@ async function getResponse(event, client, requestId) { // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet // and is not ready to handle requests. if (!activeClientIds.has(client.id)) { - return passthrough(); + return passthrough() } // Notify the client that a request has been intercepted. - const requestBuffer = await request.arrayBuffer(); + const requestBuffer = await request.arrayBuffer() const clientMessage = await sendToClient( client, { - type: "REQUEST", + type: 'REQUEST', payload: { id: requestId, url: request.url, @@ -230,38 +230,38 @@ async function getResponse(event, client, requestId) { }, }, [requestBuffer], - ); + ) switch (clientMessage.type) { - case "MOCK_RESPONSE": { - return respondWithMock(clientMessage.data); + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) } - case "PASSTHROUGH": { - return passthrough(); + case 'PASSTHROUGH': { + return passthrough() } } - return passthrough(); + return passthrough() } function sendToClient(client, message, transferrables = []) { return new Promise((resolve, reject) => { - const channel = new MessageChannel(); + const channel = new MessageChannel() channel.port1.onmessage = (event) => { if (event.data && event.data.error) { - return reject(event.data.error); + return reject(event.data.error) } - resolve(event.data); - }; + resolve(event.data) + } client.postMessage( message, [channel.port2].concat(transferrables.filter(Boolean)), - ); - }); + ) + }) } async function respondWithMock(response) { @@ -270,15 +270,15 @@ async function respondWithMock(response) { // instance will have status code set to 0. Since it's not possible to create // a Response instance with status code 0, handle that use-case separately. if (response.status === 0) { - return Response.error(); + return Response.error() } - const mockedResponse = new Response(response.body, response); + const mockedResponse = new Response(response.body, response) Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { value: true, enumerable: true, - }); + }) - return mockedResponse; + return mockedResponse } diff --git a/refact-agent/gui/src/components/Chat/Chat.tsx b/refact-agent/gui/src/components/Chat/Chat.tsx index 5f411bd69..bccdc2a26 100644 --- a/refact-agent/gui/src/components/Chat/Chat.tsx +++ b/refact-agent/gui/src/components/Chat/Chat.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from "react"; -import { ChatForm, ChatFormProps } from "../ChatForm"; +import { ChatForm } from "../ChatForm"; import { ChatContent } from "../ChatContent"; import { Flex, Button, Text, Card } from "@radix-ui/themes"; import { @@ -36,14 +36,9 @@ export type ChatProps = { backFromChat: () => void; style?: React.CSSProperties; unCalledTools: boolean; - maybeSendToSidebar: ChatFormProps["onClose"]; }; -export const Chat: React.FC = ({ - style, - unCalledTools, - maybeSendToSidebar, -}) => { +export const Chat: React.FC = ({ style, unCalledTools }) => { const dispatch = useAppDispatch(); const [isViewingRawJSON, setIsViewingRawJSON] = useState(false); @@ -142,7 +137,6 @@ export const Chat: React.FC = ({ diff --git a/refact-agent/gui/src/components/ChatForm/ChatForm.tsx b/refact-agent/gui/src/components/ChatForm/ChatForm.tsx index 56c3c3bd2..8166bcdc9 100644 --- a/refact-agent/gui/src/components/ChatForm/ChatForm.tsx +++ b/refact-agent/gui/src/components/ChatForm/ChatForm.tsx @@ -3,11 +3,7 @@ import React, { useCallback, useEffect, useMemo } from "react"; import { Flex, Card, Text } from "@radix-ui/themes"; import styles from "./ChatForm.module.css"; -import { - PaperPlaneButton, - BackToSideBarButton, - AgentIntegrationsButton, -} from "../Buttons/Buttons"; +import { PaperPlaneButton, AgentIntegrationsButton } from "../Buttons/Buttons"; import { TextArea } from "../TextArea"; import { Form } from "./Form"; import { @@ -57,14 +53,13 @@ import { AgentCapabilities } from "./AgentCapabilities"; export type ChatFormProps = { onSubmit: (str: string) => void; - onClose?: () => void; className?: string; unCalledTools: boolean; }; export const ChatForm: React.FC = ({ onSubmit, - onClose, + className, unCalledTools, }) => { @@ -362,14 +357,6 @@ export const ChatForm: React.FC = ({ ref={(x) => refs.setSetupIntegrations(x)} /> )} - {onClose && ( - - )} {config.features?.images !== false && isMultimodalitySupportedForCurrentModel && } {/* TODO: Reserved space for microphone button coming later on */} diff --git a/refact-agent/gui/src/components/ChatHistory/ChatHistory.tsx b/refact-agent/gui/src/components/ChatHistory/ChatHistory.tsx index dbd273cc7..c2d129309 100644 --- a/refact-agent/gui/src/components/ChatHistory/ChatHistory.tsx +++ b/refact-agent/gui/src/components/ChatHistory/ChatHistory.tsx @@ -6,25 +6,26 @@ import { getHistory, type HistoryState, } from "../../features/History/historySlice"; -import type { ChatThread } from "../../features/Chat/Thread/types"; +// import type { ChatThread } from "../../features/Chat/Thread/types"; export type ChatHistoryProps = { history: HistoryState; - onHistoryItemClick: (id: ChatThread) => void; + // onHistoryItemClick: (id: ChatThread) => void; onDeleteHistoryItem: (id: string) => void; onOpenChatInTab?: (id: string) => void; currentChatId?: string; }; - +// TODO: history item should be a nav link export const ChatHistory = memo( ({ history, - onHistoryItemClick, + // onHistoryItemClick, onDeleteHistoryItem, onOpenChatInTab, currentChatId, }: ChatHistoryProps) => { const sortedHistory = getHistory({ history }); + return ( {sortedHistory.map((item) => ( onHistoryItemClick(item)} + // onClick={() => onHistoryItemClick(item)} onOpenInTab={onOpenChatInTab} onDelete={onDeleteHistoryItem} key={item.id} diff --git a/refact-agent/gui/src/components/ChatHistory/HistoryItem.tsx b/refact-agent/gui/src/components/ChatHistory/HistoryItem.tsx index f3c079575..063d0e65d 100644 --- a/refact-agent/gui/src/components/ChatHistory/HistoryItem.tsx +++ b/refact-agent/gui/src/components/ChatHistory/HistoryItem.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useCallback } from "react"; import { Card, Flex, Text, Box, Spinner } from "@radix-ui/themes"; // import type { ChatHistoryItem } from "../../hooks/useChatHistory"; import { ChatBubbleIcon, DotFilledIcon } from "@radix-ui/react-icons"; @@ -8,17 +8,34 @@ import { OpenInNewWindowIcon } from "@radix-ui/react-icons"; import type { ChatHistoryItem } from "../../features/History/historySlice"; import { isUserMessage } from "../../services/refact"; import { useAppSelector } from "../../hooks"; +import { useNavigate } from "react-router"; export const HistoryItem: React.FC<{ historyItem: ChatHistoryItem; - onClick: () => void; + // onClick: () => void; onDelete: (id: string) => void; onOpenInTab?: (id: string) => void; disabled: boolean; -}> = ({ historyItem, onClick, onDelete, onOpenInTab, disabled }) => { +}> = ({ + historyItem, + // onClick, + onDelete, + onOpenInTab, + disabled, +}) => { const dateCreated = new Date(historyItem.createdAt); const dateTimeString = dateCreated.toLocaleString(); const cache = useAppSelector((app) => app.chat.cache); + const navigate = useNavigate(); + + const handleClick = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + void navigate(`/chat/${historyItem.id}`); + }, + [historyItem.id, navigate], + ); const isStreaming = historyItem.id in cache; return ( @@ -34,14 +51,7 @@ export const HistoryItem: React.FC<{ asChild role="button" > -