diff --git a/package-lock.json b/package-lock.json index 198013f96..cd7e6de0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "refact-chat-js", - "version": "2.0.7", + "version": "2.0.8-alpha.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "refact-chat-js", - "version": "2.0.7", + "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/package.json b/package.json index 303972985..9ef5f0c82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "refact-chat-js", - "version": "2.0.7", + "version": "2.0.8-alpha.2", "type": "module", "license": "BSD-3-Clause", "files": [ @@ -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/src/__fixtures__/chat.ts b/src/__fixtures__/chat.ts index 36cd5f151..854f1dba6 100644 --- a/src/__fixtures__/chat.ts +++ b/src/__fixtures__/chat.ts @@ -70,6 +70,9 @@ export const MARS_ROVER_CHAT: ChatHistoryItem = { ], title: "mars rover kata", isTitleGenerated: true, + new_chat_suggested: { + wasSuggested: false, + }, model: "gpt-3.5-turbo", tool_use: "explore", createdAt: "2023-12-21T17:32:50.186Z", @@ -394,6 +397,9 @@ export const FROG_CHAT: ChatThread = { }, ], title: "", + new_chat_suggested: { + wasSuggested: false, + }, model: "", }; @@ -578,6 +584,9 @@ export const CHAT_WITH_DIFF_ACTIONS: ChatThread = { title: "In the project add an edible property to the frog class\n", model: "gpt-4o", tool_use: "explore", + new_chat_suggested: { + wasSuggested: false, + }, createdAt: "2024-07-05T09:10:29.523Z", updatedAt: "2024-07-05T09:10:37.322Z", }; @@ -845,6 +854,9 @@ export const LARGE_DIFF: ChatThread = { title: "rename the frog class to bird.\n", model: "gpt-4o", tool_use: "explore", + new_chat_suggested: { + wasSuggested: false, + }, createdAt: "2024-07-23T15:08:51.480Z", updatedAt: "2024-07-23T15:36:26.738Z", }; @@ -911,3 +923,576 @@ export const TOOL_IMAGE_STUB: ChatMessages = [ }, }, ]; + +export const CHAT_WITH_KNOWLEDGE_TOOL: ChatThread = { + id: "88acc4df-ead5-473b-aca8-4fbd1abf4ef9", + messages: [ + { + role: "system", + content: + '[mode3] You are Refact Agent, an autonomous bot for coding tasks.\n\nInstructions of how to generate 📍-notation edit blocks:\n1. Edit Blocks\n - Wrap edit block content in triple backticks:\n \\```\n (code or file content here)\n \\```\n - Keep the original indentation exactly.\n2. Pre-Edit Instruction\n - Before each edit block, include exactly one of these lines:\n 1. 📍REWRITE_ONE_SYMBOL "" SYMBOL_NAME \n 2. 📍REWRITE_WHOLE_FILE ""\n 3. 📍PARTIAL_EDIT ""\n 4. 📍OTHER \n - ``: 3-digit number (e.g., 000, 001, 002, …).\n - ``: full path to the file.\n3. When to Use Each Command\n 1. 📍REWRITE_ONE_SYMBOL\n - Use for updating a single function, class, or method. Use it only for java, python, js, c++, rust and typescript.\n 2. 📍REWRITE_WHOLE_FILE\n - Use when replacing or creating the entire file content. Prefer it if there are many small changes.\n 3. 📍PARTIAL_EDIT\n - Use for editing or inserting code in the middle of a file.\n - Provide a few original lines above and below the edited section. This ensures clarity and reduces the risk of merging conflicts.\n 4. 📍OTHER\n - For anything that isn’t a file edit (e.g., explanations, command-line instructions).\n4. Tips\n - For multiple symbol changes, use `📍PARTIAL_EDIT`.\n - Include context lines above and below your changes for clarity.\n - Avoid rewriting the entire file if only a few edits are needed.\n - If the `patch` tool is available, only call it after creating the code block.\n - Avoid being lazy when creating the edit blocks - copy all necessary unchanged lines as they are\n5. 📍-notation format examples\n- Rewriting a single symbol:\n📍REWRITE_ONE_SYMBOL 001 "C:\\Users\\username\\sales\\OrderCalculator.java" calculateTotal OrderCalculator::calculateTotal\n```\n public double calculateTotal(Order order) {\n double sum = 0;\n for (LineItem item : order.getItems()) {\n sum += item.getPrice() * item.getQuantity();\n }\n return sum;\n }\n```\n\n- Rewriting (or creating) the whole file:\n📍REWRITE_WHOLE_FILE 002 "/home/username/scripts/data_processing.py"\n```\n#!/usr/bin/env python3\n\nimport sys\n\ndef main():\n print("New script content")\n\nif __name__ == "__main__":\n main()\n```\n\n- Partial edit inside a file:\n📍PARTIAL_EDIT 003 "/Users/username/app.js"\n```\n router.post(\'/api/items\', async (req, res) => {\n try {\n const validatedData = await validateInput(req.body);\n const result = await processItems(req.body);\n res.json(result);\n } catch (error) {\n res.status(500).send(error.message);\n }\n });\n```\n\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, write the changes using 📍-notation.\n - Then call patch() for each 📍-notation ticket.\n - Separate multiple tickets (changes) with commas if they are related to a single file.\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 desinged 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: "rename frog to bird\n", + }, + { + role: "assistant", + content: + 'Let me help you rename "frog" to "bird" in the project. First, I\'ll check what we\'re working with and get some guidance.', + tool_calls: [ + { + id: "toolu_01QjezACFfkEe4Yfid2AgdPh", + function: { + arguments: + '{"goal": "Rename \'frog\' to \'bird\' in project files", "language_slash_framework": "unknown", "im_going_to_apply_to": "emergency_frog_situation", "im_going_to_use_tools": "search, cat, locate"}', + name: "knowledge", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "toolu_01QjezACFfkEe4Yfid2AgdPh", + content: + '🗃️110c57fd71\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🗃️019957b6ff\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🗃️36338b63b3\n[\n["goal", "Discuss whether birds are real, their software, programming, and Python usage"],\n["thinking", "User is asking about birds and software. Evidence: birds are biological creatures, but there\'s research into bird-inspired algorithms and robotics."],\n["thinking", "When asked about bird programming, focused on research projects like BirdBrain, Flocking, and RoboBird that simulate or interact with birds."],\n["thinking", "When asked about Python-using birds, clarified that birds don\'t use programming languages, but Python is used by researchers to study birds."],\n["coding", "Provided example of Boid algorithm simulation in Python showing flocking behavior"],\n["coding", "Provided finite state machine simulation of bird behavior states (perched, flying, eating)"],\n["coding", "Provided bird population growth simulation using simple mathematical model"],\n["coding", "Provided example of bird song classification using RandomForestClassifier"],\n["outcome", "SUCCESS"]\n]\n\n🗃️81e825a188\n[\n["goal", "Add swim method to Frog class in frog.py"],\n["thinking", "Can add swim method directly using REWRITE_ONE_SYMBOL since the file is small and class structure is clear"],\n["coding", "📍REWRITE_ONE_SYMBOL 000 added swim(dx, dy, pond_width, pond_height) method with position updates and boundary checks"],\n["outcome", "SUCCESS"]\n]\n\n🗃️6f3566503d\nLooks like proj2 is written in fact in Rust.\n', + }, + }, + { + role: "assistant", + content: "\n\nLet me first locate the relevant files for this task.", + tool_calls: [ + { + id: "toolu_01P9sbpcJDR7tDBFPDVbRuYK", + function: { + arguments: '{"problem_statement": "rename frog to bird"}', + name: "locate", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "toolu_01P9sbpcJDR7tDBFPDVbRuYK", + content: + '{\n "FOUND": {\n "frog.py": "Frog",\n "holiday.py": "frog.Frog",\n "work_day.py": "bring_your_own_frog"\n },\n "MORE_TOCHANGE": {\n "set_as_avatar.py": "Toad, EuropeanCommonToad",\n "jump_to_conclusions.py": "creatures",\n "holiday.py": "frog1,frog2"\n },\n "USAGE": {\n "jump_to_conclusions.py": "creatures",\n "work_day.py": "bring_your_own_frog",\n "set_as_avatar.py": "Toad, EuropeanCommonToad"\n }\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, + }, + { + file_name: "holiday.py", + file_content: + "import frog\n\n\nif __name__ == __main__:\n frog1 = frog.Frog()\n frog2 = frog.Frog()\n\n # First jump\n frog1.jump()\n frog2.jump()\n\n # Second jump\n frog1.jump()\n frog2.jump()\n\n # Third jump\n frog1.jump()\n frog2.jump()\n\n # Forth jump\n frog1.jump()\n frog2.jump()\n", + line1: 1, + line2: 21, + }, + { + file_name: "work_day.py", + file_content: + "# Picking up context, goal in this file:\n# - without any other information, find method usage in another file by text similarity\n\nimport numpy as np\nimport frog\n\nX,Y = 50, 50\nW = 100\nH = 100\n\ndef bring_your_own_frog(f: frog.Frog):\n f.jump(W, H)\n", + line1: 1, + line2: 11, + }, + { + file_name: "set_as_avatar.py", + file_content: + '...\nclass Toad(frog.Frog):\n def __init__(self, x, y, vx, vy):\n super().__init__(x, y, vx, vy)\n self.known_as = "Bob"\n self.croak()\n\n def hello_world(self):\n self.croak()\n...\nclass EuropeanCommonToad(frog.Frog):\n """\n This is a comment for EuropeanCommonToad class, inside the class\n """\n\n def __init__(self, x, y, vx, vy):\n super().__init__(x, y, vx, vy)\n self.known_as = "EU Toad"\n...\ndef some_fun(f1: Toad, f2: EuropeanCommonToad, f3: frog.Frog, f4):\n f1.croak()\n f2.croak()\n f3.croak()\n f4.croak()\n\ndef use_some_variables(f1: Toad, f2: EuropeanCommonToad, f3: frog.Frog, f4):\n print(f1.known_as)\n print(f2.known_as)\n print(f3.known_as) # there isn\'t one in Frog!\n print(f3.x) # but it has x\n print(f4.y) # no type, can\'t resolve\n\ndef a_bigger_test():\n f1 = Toad(110, 110, 0.2, 0.4)\n f2 = EuropeanCommonToad(120, 120, 0.3, 0.4)\n f3 = frog.Frog(130, 130, 0.4, 0.6)\n f4 = f3\n some_fun(f1, f2, f3, f4)\n use_some_variables(f1, f2, f3, f4)\n...\n', + line1: 12, + line2: 57, + }, + { + file_name: "jump_to_conclusions.py", + file_content: + '...\ndef draw_hello_frog(\n screen: pygame.Surface,\n message: str,\n color: Tuple[int, int, int] = (0, 255, 255),\n font_name: str = "Arial",\n) -> None:\n font = pygame.font.SysFont(font_name, 32)\n text = font.render(message, True, color)\n text_rect = text.get_rect()\n text_rect.center = (W / 2, H / 2)\n screen.blit(text, text_rect)\n...\ncreatures = [\n...\ndef main_loop():\n screen = pygame.display.set_mode((W,H)) # without space because it\'s a test it needs to pick up the correct line below\n quit_flag = False\n while not quit_flag:\n for event in pygame.event.get():\n if event.type == pygame.QUIT:\n quit_flag = True\n screen.fill((0, 0, 0))\n for p in creatures:\n pygame.draw.circle(screen, (0, 255, 0), (p.x, p.y), 10)\n draw_hello_frog(screen, "Jump To Conclusions!", (0, 200, 0))\n p: frog.Frog\n for p in creatures:\n p.jump(W, H)\n pygame.display.flip()\n pygame.time.Clock().tick(60)\n...\n', + line1: 14, + line2: 58, + }, + ], + }, + { + role: "cd_instruction", + content: + "💿 locate() looked inside of 5 files, workspace has 5 files. Files relevant to the task were attached above. Don't call cat() for the same files, you already have them. Follow your task and the system prompt. ", + }, + { + role: "assistant", + content: + 'I\'ll help rename "frog" to "bird" throughout the project. Based on the files provided, I need to make several changes. Let me handle this systematically.\n\n1. First, let\'s rename the main frog.py file and its classes:\n\n📍REWRITE_WHOLE_FILE 001 "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/frog.py"\n```\nimport numpy as np\n\nDT = 0.01\n\nclass Bird:\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("chirp")\n\n\nclass AlternativeBird:\n def alternative_jump(self):\n pass\n\n\ndef standalone_jumping_function():\n print("I\'m a bird! Fly! Fly! Really!")\n```\n\n2. Update holiday.py:\n\n📍REWRITE_WHOLE_FILE 002 "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/holiday.py"\n```\nimport bird\n\n\nif __name__ == __main__:\n bird1 = bird.Bird()\n bird2 = bird.Bird()\n\n # First jump\n bird1.jump()\n bird2.jump()\n\n # Second jump\n bird1.jump()\n bird2.jump()\n\n # Third jump\n bird1.jump()\n bird2.jump()\n\n # Forth jump\n bird1.jump()\n bird2.jump()\n```\n\n3. Update work_day.py:\n\n📍REWRITE_WHOLE_FILE 003 "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/work_day.py"\n```\n# Picking up context, goal in this file:\n# - without any other information, find method usage in another file by text similarity\n\nimport numpy as np\nimport bird\n\nX,Y = 50, 50\nW = 100\nH = 100\n\ndef bring_your_own_bird(f: bird.Bird):\n f.jump(W, H)\n```\n\n4. Update set_as_avatar.py:\n\n📍REWRITE_WHOLE_FILE 004 "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/set_as_avatar.py"\n```\nclass Sparrow(bird.Bird):\n def __init__(self, x, y, vx, vy):\n super().__init__(x, y, vx, vy)\n self.known_as = "Bob"\n self.croak()\n\n def hello_world(self):\n self.croak()\n\nclass EuropeanCommonSparrow(bird.Bird):\n """\n This is a comment for EuropeanCommonSparrow class, inside the class\n """\n\n def __init__(self, x, y, vx, vy):\n super().__init__(x, y, vx, vy)\n self.known_as = "EU Sparrow"\n\ndef some_fun(f1: Sparrow, f2: EuropeanCommonSparrow, f3: bird.Bird, f4):\n f1.croak()\n f2.croak()\n f3.croak()\n f4.croak()\n\ndef use_some_variables(f1: Sparrow, f2: EuropeanCommonSparrow, f3: bird.Bird, f4):\n print(f1.known_as)\n print(f2.known_as)\n print(f3.known_as) # there isn\'t one in Bird!\n print(f3.x) # but it has x\n print(f4.y) # no type, can\'t resolve\n\ndef a_bigger_test():\n f1 = Sparrow(110, 110, 0.2, 0.4)\n f2 = EuropeanCommonSparrow(120, 120, 0.3, 0.4)\n f3 = bird.Bird(130, 130, 0.4, 0.6)\n f4 = f3\n some_fun(f1, f2, f3, f4)\n use_some_variables(f1, f2, f3, f4)\n```\n\n5. Update jump_to_conclusions.py:\n\n📍REWRITE_WHOLE_FILE 005 "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/jump_to_conclusions.py"\n```\ndef draw_hello_bird(\n screen: pygame.Surface,\n message: str,\n color: Tuple[int, int, int] = (0, 255, 255),\n font_name: str = "Arial",\n) -> None:\n font = pygame.font.SysFont(font_name, 32)\n text = font.render(message, True, color)\n text_rect = text.get_rect()\n text_rect.center = (W / 2, H / 2)\n screen.blit(text, text_rect)\n\ncreatures = [\n\ndef main_loop():\n screen = pygame.display.set_mode((W,H)) # without space because it\'s a test it needs to pick up the correct line below\n quit_flag = False\n while not quit_flag:\n for event in pygame.event.get():\n if event.type == pygame.QUIT:\n quit_flag = True\n screen.fill((0, 0, 0))\n for p in creatures:\n pygame.draw.circle(screen, (0, 255, 0), (p.x, p.y), 10)\n draw_hello_bird(screen, "Fly To Conclusions!", (0, 200, 0))\n p: bird.Bird\n for p in creatures:\n p.jump(W, H)\n pygame.display.flip()\n pygame.time.Clock().tick(60)\n```\n\nNow let\'s apply all these changes:', + tool_calls: [ + { + id: "toolu_01S5Sy6j4LFeNA6zJ6pq9B7H", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/frog.py", "tickets": "001"}', + name: "patch", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/frog.py", + file_action: "edit", + line1: 5, + line2: 6, + lines_remove: "class Frog:\n", + lines_add: "class Bird:\n", + file_name_rename: null, + application_details: "Chunk applied successfully", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/frog.py", + file_action: "edit", + line1: 31, + line2: 32, + lines_remove: ' print("croak")\n', + lines_add: ' print("chirp")\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/frog.py", + file_action: "edit", + line1: 34, + line2: 35, + lines_remove: "class AlternativeFrog:\n", + lines_add: "class AlternativeBird:\n", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/frog.py", + file_action: "edit", + line1: 40, + line2: 42, + lines_remove: ' print("I\'m a frog! Jump! Jump! Really!")\n\n', + lines_add: ' print("I\'m a bird! Fly! Fly! Really!")\n', + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_01S5Sy6j4LFeNA6zJ6pq9B7H", + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "toolu_012Bp65uk46rTTza4RQKhXbT", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/holiday.py", "tickets": "002"}', + name: "patch", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/holiday.py", + file_action: "edit", + line1: 1, + line2: 2, + lines_remove: "import frog\n", + lines_add: "import bird\n", + file_name_rename: null, + application_details: "Chunk applied successfully", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/holiday.py", + file_action: "edit", + line1: 5, + line2: 7, + lines_remove: " frog1 = frog.Frog()\n frog2 = frog.Frog()\n", + lines_add: " bird1 = bird.Bird()\n bird2 = bird.Bird()\n", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/holiday.py", + file_action: "edit", + line1: 9, + line2: 11, + lines_remove: " frog1.jump()\n frog2.jump()\n", + lines_add: " bird1.jump()\n bird2.jump()\n", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/holiday.py", + file_action: "edit", + line1: 13, + line2: 15, + lines_remove: " frog1.jump()\n frog2.jump()\n", + lines_add: " bird1.jump()\n bird2.jump()\n", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/holiday.py", + file_action: "edit", + line1: 17, + line2: 19, + lines_remove: " frog1.jump()\n frog2.jump()\n", + lines_add: " bird1.jump()\n bird2.jump()\n", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/holiday.py", + file_action: "edit", + line1: 21, + line2: 24, + lines_remove: " frog1.jump()\n frog2.jump()\n\n", + lines_add: " bird1.jump()\n bird2.jump()\n", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_012Bp65uk46rTTza4RQKhXbT", + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "toolu_01Xt5XDnwoijEFZAXi8PEYcQ", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/work_day.py", "tickets": "003"}', + name: "patch", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/work_day.py", + file_action: "edit", + line1: 5, + line2: 6, + lines_remove: "import frog\n", + lines_add: "import bird\n", + file_name_rename: null, + application_details: "Chunk applied successfully", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/work_day.py", + file_action: "edit", + line1: 11, + line2: 12, + lines_remove: "def bring_your_own_frog(f: frog.Frog):\n", + lines_add: "def bring_your_own_bird(f: bird.Bird):\n", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/work_day.py", + file_action: "edit", + line1: 13, + line2: 14, + lines_remove: "\n", + lines_add: "", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_01Xt5XDnwoijEFZAXi8PEYcQ", + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "toolu_01PqV7mYbFyJfsg7Wc6TRnwd", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/set_as_avatar.py", "tickets": "004"}', + name: "patch", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/set_as_avatar.py", + file_action: "edit", + line1: 1, + line2: 14, + lines_remove: + "# Picking up context, goal in this file:\n# - goto parent class, two times\n# - dump parent class\n\nimport frog\n\nX, Y = 50, 50\nW = 100\nH = 100\n\n\n# This this a comment for the Toad class, above the class\nclass Toad(frog.Frog):\n", + lines_add: "class Sparrow(bird.Bird):\n", + file_name_rename: null, + application_details: "Chunk applied successfully", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/set_as_avatar.py", + file_action: "edit", + line1: 22, + line2: 24, + lines_remove: "\nclass EuropeanCommonToad(frog.Frog):\n", + lines_add: "class EuropeanCommonSparrow(bird.Bird):\n", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/set_as_avatar.py", + file_action: "edit", + line1: 25, + line2: 26, + lines_remove: + " This is a comment for EuropeanCommonToad class, inside the class\n", + lines_add: + " This is a comment for EuropeanCommonSparrow class, inside the class\n", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/set_as_avatar.py", + file_action: "edit", + line1: 30, + line2: 31, + lines_remove: ' self.known_as = "EU Toad"\n', + lines_add: ' self.known_as = "EU Sparrow"\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/set_as_avatar.py", + file_action: "edit", + line1: 32, + line2: 34, + lines_remove: + "\ndef some_fun(f1: Toad, f2: EuropeanCommonToad, f3: frog.Frog, f4):\n", + lines_add: + "def some_fun(f1: Sparrow, f2: EuropeanCommonSparrow, f3: bird.Bird, f4):\n", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/set_as_avatar.py", + file_action: "edit", + line1: 39, + line2: 40, + lines_remove: + "def use_some_variables(f1: Toad, f2: EuropeanCommonToad, f3: frog.Frog, f4):\n", + lines_add: + "def use_some_variables(f1: Sparrow, f2: EuropeanCommonSparrow, f3: bird.Bird, f4):\n", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/set_as_avatar.py", + file_action: "edit", + line1: 42, + line2: 43, + lines_remove: " print(f3.known_as) # there isn't one in Frog!\n", + lines_add: " print(f3.known_as) # there isn't one in Bird!\n", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/set_as_avatar.py", + file_action: "edit", + line1: 47, + line2: 50, + lines_remove: + " f1 = Toad(110, 110, 0.2, 0.4)\n f2 = EuropeanCommonToad(120, 120, 0.3, 0.4)\n f3 = frog.Frog(130, 130, 0.4, 0.6)\n", + lines_add: + " f1 = Sparrow(110, 110, 0.2, 0.4)\n f2 = EuropeanCommonSparrow(120, 120, 0.3, 0.4)\n f3 = bird.Bird(130, 130, 0.4, 0.6)\n", + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/set_as_avatar.py", + file_action: "edit", + line1: 53, + line2: 60, + lines_remove: + '\n\nif __name__ == "__main__":\n toad = EuropeanCommonToad(100, 100, 200, -200)\n toad.jump(W, H)\n print(toad.known_as, toad.x, toad.y)\n\n', + lines_add: "", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_01PqV7mYbFyJfsg7Wc6TRnwd", + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "toolu_01XrmGSBgvr3BNHw8VrNM2M5", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/jump_to_conclusions.py", "tickets": "005"}', + name: "patch", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "toolu_01XrmGSBgvr3BNHw8VrNM2M5", + content: + 'AST assessment has failed: the generated diff had introduced errors into the file `"/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/jump_to_conclusions.py"`: 0 before errs < 46 after errs', + }, + }, + { + role: "assistant", + content: + '\n\nI apologize for the error with jump_to_conclusions.py. Let me fix it with a more careful partial edit:\n\n📍PARTIAL_EDIT 006 "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/jump_to_conclusions.py"\n```\ndef draw_hello_bird(\n screen: pygame.Surface,\n message: str,\n color: Tuple[int, int, int] = (0, 255, 255),\n font_name: str = "Arial",\n) -> None:\n font = pygame.font.SysFont(font_name, 32)\n text = font.render(message, True, color)\n text_rect = text.get_rect()\n text_rect.center = (W / 2, H / 2)\n screen.blit(text, text_rect)\n\ncreatures = []\n\ndef main_loop():\n screen = pygame.display.set_mode((W,H))\n quit_flag = False\n while not quit_flag:\n for event in pygame.event.get():\n if event.type == pygame.QUIT:\n quit_flag = True\n screen.fill((0, 0, 0))\n for p in creatures:\n pygame.draw.circle(screen, (0, 255, 0), (p.x, p.y), 10)\n draw_hello_bird(screen, "Fly To Conclusions!", (0, 200, 0))\n p: bird.Bird\n for p in creatures:\n p.jump(W, H)\n pygame.display.flip()\n pygame.time.Clock().tick(60)\n```', + tool_calls: [ + { + id: "toolu_01N3pBz4bQmU2ETxzRM1v1iN", + function: { + arguments: + '{"path": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/jump_to_conclusions.py", "tickets": "006"}', + name: "patch", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "diff", + content: [ + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/jump_to_conclusions.py", + file_action: "edit", + line1: 15, + line2: 35, + lines_remove: + 'def draw_hello_frog(\n screen: pygame.Surface,\n message: str,\n color: Tuple[int, int, int] = (0, 255, 255),\n font_name: str = "Arial",\n) -> None:\n font = pygame.font.SysFont(font_name, 32)\n text = font.render(message, True, color)\n text_rect = text.get_rect()\n text_rect.center = (W / 2, H / 2)\n screen.blit(text, text_rect)\n\n\ncreatures = [\n frog.Frog(\n np.random.uniform(0, W),\n np.random.uniform(0, H),\n np.random.uniform(-W/10, H/10),\n np.random.uniform(-W/10, H/10),\n ) for i in range(10)]\n', + lines_add: + 'def draw_hello_bird(\n screen: pygame.Surface,\n message: str,\n color: Tuple[int, int, int] = (0, 255, 255),\n font_name: str = "Arial",\n) -> None:\n font = pygame.font.SysFont(font_name, 32)\n text = font.render(message, True, color)\n text_rect = text.get_rect()\n text_rect.center = (W / 2, H / 2)\n screen.blit(text, text_rect)\n\ncreatures = []\n', + file_name_rename: null, + application_details: "Chunk applied successfully", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/jump_to_conclusions.py", + file_action: "edit", + line1: 47, + line2: 48, + lines_remove: + ' draw_hello_frog(screen, "Jump To Conclusions!", (0, 200, 0))\n', + lines_add: + ' draw_hello_bird(screen, "Fly To Conclusions!", (0, 200, 0))\n', + file_name_rename: null, + application_details: "", + }, + { + file_name: + "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/jump_to_conclusions.py", + file_action: "edit", + line1: 48, + line2: 49, + lines_remove: " p: frog.Frog\n", + lines_add: " p: bird.Bird\n", + file_name_rename: null, + application_details: "", + }, + ], + tool_call_id: "toolu_01N3pBz4bQmU2ETxzRM1v1iN", + }, + { + role: "assistant", + content: "\n\nFinally, we need to rename the frog.py file to bird.py:", + tool_calls: [ + { + id: "toolu_01EkpiymGNGZPdzevMeTpRS9", + function: { + arguments: + '{"command": "mv /Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/frog.py /Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/bird.py", "workdir": "/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation"}', + name: "shell", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "toolu_01EkpiymGNGZPdzevMeTpRS9", + content: + "Nothing in STDOUT/STDERR\n\nThe command was running 0.010s, finished with exit code 0", + }, + }, + { + role: "assistant", + content: + '\n\nI\'ve completed the renaming task:\n1. Renamed the main class from `Frog` to `Bird`\n2. Renamed the file from `frog.py` to `bird.py`\n3. Updated all references in other files\n4. Changed related text like "croak" to "chirp" and "Jump" to "Fly"\n5. Updated class names like `Toad` to `Sparrow` and `EuropeanCommonToad` to `EuropeanCommonSparrow`\n6. Updated all import statements and type hints\n7. Updated function names and variables to use "bird" instead of "frog"\n\nAll changes have been applied successfully. The project should now use "bird" terminology consistently throughout the codebase.', + }, + ], + title: "Rename Frog Bird", + model: "", + tool_use: "agent", + mode: "AGENT", + read: true, + isTitleGenerated: true, + new_chat_suggested: { + wasSuggested: false, + }, + createdAt: "2025-01-27T13:19:11.132Z", + updatedAt: "2025-01-27T13:19:11.132Z", +}; diff --git a/src/__fixtures__/chat_config_thread.ts b/src/__fixtures__/chat_config_thread.ts index 77912da5b..9bd824400 100644 --- a/src/__fixtures__/chat_config_thread.ts +++ b/src/__fixtures__/chat_config_thread.ts @@ -459,6 +459,9 @@ export const CHAT_CONFIG_THREAD: Chat = { project: "", }, read: true, + new_chat_suggested: { + wasSuggested: false, + }, createdAt: "2024-12-02T14:42:18.902Z", updatedAt: "2024-12-02T14:42:18.902Z", }, diff --git a/src/__fixtures__/chat_links_response.ts b/src/__fixtures__/chat_links_response.ts index be4162ada..c2ba4470f 100644 --- a/src/__fixtures__/chat_links_response.ts +++ b/src/__fixtures__/chat_links_response.ts @@ -3,7 +3,7 @@ import { LinksForChatResponse } from "../services/refact/links"; export const STUB_LINKS_FOR_CHAT_RESPONSE: LinksForChatResponse = { uncommited_changes_warning: "You have uncommitted changes:\n```\nIn project refact-lsp: A tests/emergency_frog_situation/.refact/project_summary.yaml, M tests/emergency_frog_situation/frog.py, M tests/emergency_frog_situation/jump_to_conclusions.py, ...\n```\n⚠️ You might have a problem rolling back agent's changes.", - + new_chat_suggestion: false, links: [ { link_text: "Save and return", diff --git a/src/__fixtures__/chat_textdoc.ts b/src/__fixtures__/chat_textdoc.ts new file mode 100644 index 000000000..cd2b13e97 --- /dev/null +++ b/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", +}; diff --git a/src/__fixtures__/history.ts b/src/__fixtures__/history.ts index faf55e65e..d8d0f7050 100644 --- a/src/__fixtures__/history.ts +++ b/src/__fixtures__/history.ts @@ -28,6 +28,9 @@ export const HISTORY: ChatHistoryItem[] = [ "Write a program that solves word-chain puzzles.\n\nThere’s a type of puzzle where the challenge is to build a chain of words, starting with one particular word and ending with another. Successive entries in the chain must all be real words, and each can differ from the previous word by just one letter. For example, you can get from “cat” to “dog” using the following chain.\n", model: "", isTitleGenerated: true, + new_chat_suggested: { + wasSuggested: false, + }, createdAt: "2024-07-02T10:43:13.401Z", updatedAt: "2024-07-02T10:44:38.325Z", tool_use: "explore", @@ -115,6 +118,9 @@ export const HISTORY: ChatHistoryItem[] = [ "In this project, what is the difference between a toad and a frog?\n", model: "", isTitleGenerated: true, + new_chat_suggested: { + wasSuggested: false, + }, createdAt: "2024-07-02T10:40:27.354Z", updatedAt: "2024-07-02T10:40:32.341Z", tool_use: "explore", diff --git a/src/__fixtures__/index.ts b/src/__fixtures__/index.ts index 4110e97d9..b50208fc5 100644 --- a/src/__fixtures__/index.ts +++ b/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/src/__fixtures__/knowledge.ts b/src/__fixtures__/knowledge.ts new file mode 100644 index 000000000..f2446e513 --- /dev/null +++ b/src/__fixtures__/knowledge.ts @@ -0,0 +1,264 @@ +import type { + MemdbSubEvent, + MemoRecord, + VecDbStatus, +} from "../services/refact"; + +export const STUB_MEMORIES: MemoRecord[] = [ + { + memid: "7666487b81", + m_type: "proj-fact", + m_goal: "compile", + m_project: "proj1", + m_payload: "Looks like proj1 is written in fact in Rust.", + m_origin: "local-committed", + mstat_correct: 1, + mstat_relevant: -1, + mstat_times_used: 1, + }, + { + memid: "cdec854819", + m_type: "seq-of-acts", + m_goal: "compile", + m_project: "proj2", + m_payload: "Wow, running cargo build on proj2 was successful!", + m_origin: "local-committed", + mstat_correct: 0, + mstat_relevant: 0, + mstat_times_used: 0, + }, + { + memid: "eb1d64684b", + m_type: "proj-fact", + m_goal: "compile", + m_project: "proj2", + m_payload: "Looks like proj2 is written in fact in Rust.", + m_origin: "local-committed", + mstat_correct: 0, + mstat_relevant: 0, + mstat_times_used: 0, + }, + + { + memid: "eb1d64684c", + m_type: "proj-fact", + m_goal: + "Long goal Long goal Long goal Long goal Long goal Long goal Long goal Long goal Long goal Long goal", + m_project: "proj2", + m_payload: "Looks like proj2 is written in fact in Rust.", + m_origin: "local-committed", + mstat_correct: 0, + mstat_relevant: 0, + mstat_times_used: 0, + }, +]; + +export const STUB_SUB_RESPONSE: MemdbSubEvent[] = [ + { + pubevent_id: 19, + pubevent_action: "INSERT", + pubevent_json: { + memid: "66a072d699", + m_type: "seq-of-acts", + m_goal: "compile", + m_project: "proj1", + m_payload: "Wow, running cargo build on proj1 was successful!", + m_origin: "local-committed", + mstat_correct: 0.0, + mstat_relevant: 0.0, + mstat_times_used: 0, + }, + }, + { + pubevent_id: 26, + pubevent_action: "INSERT", + pubevent_json: { + memid: "d688925823", + m_type: "proj-fact", + m_goal: "compile", + m_project: "proj1", + m_payload: "Looks like proj1 is written in fact in Rust.", + m_origin: "local-committed", + mstat_correct: 0.0, + mstat_relevant: 0.0, + mstat_times_used: 0, + }, + }, + { + pubevent_id: 27, + pubevent_action: "INSERT", + pubevent_json: { + memid: "08f9374753", + m_type: "seq-of-acts", + m_goal: "compile", + m_project: "proj2", + m_payload: "Wow, running cargo build on proj2 was successful!", + m_origin: "local-committed", + mstat_correct: 0.0, + mstat_relevant: 0.0, + mstat_times_used: 0, + }, + }, + { + pubevent_id: 28, + pubevent_action: "INSERT", + pubevent_json: { + memid: "c9cefe3ff4", + m_type: "proj-fact", + m_goal: "compile", + m_project: "proj2", + m_payload: "Looks like proj2 is written in fact in Rust.", + m_origin: "local-committed", + mstat_correct: 0.0, + mstat_relevant: 0.0, + mstat_times_used: 0, + }, + }, + { + pubevent_id: 29, + pubevent_action: "UPDATE", + pubevent_json: { + memid: "d688925823", + m_type: "proj-fact", + m_goal: "compile", + m_project: "proj1", + m_payload: "Looks like proj1 is written in fact in Rust.", + m_origin: "local-committed", + mstat_correct: 1.0, + mstat_relevant: -1.0, + mstat_times_used: 1, + }, + }, + { + pubevent_id: 30, + pubevent_action: "DELETE", + pubevent_json: { + memid: "9d2a679b09", + m_type: "", + m_goal: "", + m_project: "", + m_payload: "", + m_origin: "", + mstat_correct: 0, + mstat_relevant: 0, + mstat_times_used: 0, + }, + }, +]; + +// export const STUB_SUB_RESPONSE_WITH_STATUS: ( +// | MemdbSubEventUnparsed +// | VecDbStatus +// )[] = []; + +export const VECDB_STATUS_STARTING: VecDbStatus = { + files_unprocessed: 0, + files_total: 0, + requests_made_since_start: 1, + vectors_made_since_start: 33, + db_size: 33, + db_cache_size: 37, + state: "starting", + queue_additions: false, + vecdb_max_files_hit: false, + vecdb_errors: {}, +}; + +export const VECDB_STATUS_PARSING: VecDbStatus = { + files_unprocessed: 377, + files_total: 404, + requests_made_since_start: 5, + vectors_made_since_start: 296, + db_size: 168, + db_cache_size: 333, + state: "parsing", + queue_additions: false, + vecdb_max_files_hit: false, + vecdb_errors: {}, +}; + +export const VECDB_STATUS_PARSING_2: VecDbStatus = { + files_unprocessed: 372, + files_total: 404, + requests_made_since_start: 6, + vectors_made_since_start: 303, + db_size: 303, + db_cache_size: 340, + state: "parsing", + queue_additions: false, + vecdb_max_files_hit: false, + vecdb_errors: {}, +}; + +export const VECDV_STATUS_PARISING_3: VecDbStatus = { + files_unprocessed: 192, + files_total: 404, + requests_made_since_start: 21, + vectors_made_since_start: 990, + db_size: 1021, + db_cache_size: 1027, + state: "parsing", + queue_additions: false, + vecdb_max_files_hit: false, + vecdb_errors: {}, +}; + +export const VECDB_STATUS_PARSING_4: VecDbStatus = { + files_unprocessed: 12, + files_total: 404, + requests_made_since_start: 52, + vectors_made_since_start: 2494, + db_size: 2524, + db_cache_size: 2531, + state: "parsing", + queue_additions: false, + vecdb_max_files_hit: false, + vecdb_errors: {}, +}; + +export const VECDB_STATUS_COOLDOWN: VecDbStatus = { + files_unprocessed: 1, + files_total: 404, + requests_made_since_start: 52, + vectors_made_since_start: 2494, + db_size: 2524, + db_cache_size: 2533, + state: "cooldown", + queue_additions: false, + vecdb_max_files_hit: false, + vecdb_errors: {}, +}; + +export const VECDB_STATUS_DONE: VecDbStatus = { + files_unprocessed: 0, + files_total: 0, + requests_made_since_start: 54, + vectors_made_since_start: 2535, + db_size: 2629, + db_cache_size: 2574, + state: "done", + queue_additions: false, + vecdb_max_files_hit: false, + vecdb_errors: {}, +}; + +export const STUB_SUB_RESPONSE_WITH_STATUS = [ + VECDB_STATUS_STARTING, + // ...STUB_SUB_RESPONSE, + VECDB_STATUS_PARSING, + VECDB_STATUS_PARSING_2, + VECDV_STATUS_PARISING_3, + VECDB_STATUS_PARSING_4, + VECDB_STATUS_COOLDOWN, + VECDB_STATUS_DONE, +]; + +export const STB_LOADING_VECDB = { + VECDB_STATUS_STARTING, + VECDB_STATUS_PARSING, + VECDB_STATUS_PARSING_2, + VECDV_STATUS_PARISING_3, + VECDB_STATUS_PARSING_4, + VECDB_STATUS_COOLDOWN, + VECDB_STATUS_DONE, +}; diff --git a/src/__fixtures__/msw.ts b/src/__fixtures__/msw.ts index 71708fd6f..aedc22052 100644 --- a/src/__fixtures__/msw.ts +++ b/src/__fixtures__/msw.ts @@ -5,9 +5,16 @@ import { STUB_LINKS_FOR_CHAT_RESPONSE } from "./chat_links_response"; import { AT_TOOLS_AVAILABLE_URL, CHAT_LINKS_URL, + KNOWLEDGE_CREATE_URL, + KNOWLEDGE_SUB_URL, + KNOWLEDGE_UPDATE_USED_URL, } from "../services/refact/consts"; import { STUB_TOOL_RESPONSE } from "./tools_response"; +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", @@ -103,6 +110,18 @@ export const chatLinks: HttpHandler = http.post( }, ); +export const noChatLinks: HttpHandler = http.post( + `http://127.0.0.1:8001${CHAT_LINKS_URL}`, + () => { + const res: LinksForChatResponse = { + uncommited_changes_warning: "", + new_chat_suggestion: false, + links: [], + }; + return HttpResponse.json(res); + }, +); + export const goodTools: HttpHandler = http.get( `http://127.0.0.1:8001${AT_TOOLS_AVAILABLE_URL}`, () => { @@ -110,6 +129,88 @@ export const goodTools: HttpHandler = http.get( }, ); +export const knowLedgeLoading: HttpHandler = http.post( + `http://127.0.0.1:8001${KNOWLEDGE_SUB_URL}`, + async () => { + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + start(controller) { + // Encode the string chunks using "TextEncoder". + STUB_SUB_RESPONSE.forEach((item) => { + const str = `data: ${JSON.stringify(item)}\n\n`; + controller.enqueue(encoder.encode(str)); + }); + + controller.close(); + }, + }); + + await new Promise((resolve) => setTimeout(resolve, 3000)); + + return new HttpResponse(stream, { + headers: { + "Content-Type": "text/plain", + }, + }); + }, +); + +export const KnowledgeWithStatus: HttpHandler = http.post( + `http://127.0.0.1:8001${KNOWLEDGE_SUB_URL}`, + () => { + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + async start(controller) { + // Encode the string chunks using "TextEncoder". + for (const item of STUB_SUB_RESPONSE_WITH_STATUS) { + const str = `data: ${JSON.stringify(item)}\n\n`; + controller.enqueue(encoder.encode(str)); + await new Promise((resolve) => setTimeout(resolve, 3000)); // 1-second delay + } + + controller.close(); + }, + }); + + return new HttpResponse(stream, { + headers: { + "Content-Type": "text/plain", + }, + }); + }, +); + +export const makeKnowledgeFromChat: HttpHandler = http.post( + `http://127.0.0.1:8001${KNOWLEDGE_CREATE_URL}`, + () => { + const result: CompressTrajectoryResponse = { + memid: "foo", + trajectory: "something", + }; + return HttpResponse.json(result); + }, +); + +export const updateKnowledgeUse: HttpHandler = http.post( + `http://127.0.0.1:8001${KNOWLEDGE_UPDATE_USED_URL}`, + async (ctx) => { + const body = await ctx.request.json(); + const memid = + body && + typeof body === "object" && + "memid" in body && + typeof body.memid === "string" + ? body.memid + : "foo"; + const result: CompressTrajectoryResponse = { + memid, + trajectory: "something", + }; + + await new Promise((r) => setTimeout(r, 1000)); + return HttpResponse.json(result); + }, +); export const loginPollingGood: HttpHandler = http.get( "https://www.smallcloud.ai/v1/streamlined-login-recall-ticket", () => { @@ -187,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/src/__fixtures__/some_chrome_screenshots.ts b/src/__fixtures__/some_chrome_screenshots.ts index 55563c2ff..0bc4f80a3 100644 --- a/src/__fixtures__/some_chrome_screenshots.ts +++ b/src/__fixtures__/some_chrome_screenshots.ts @@ -2,6 +2,9 @@ import { ChatThread } from "../features/Chat"; export const CHAT_WITH_MULTI_MODAL: ChatThread = { id: "aa7cbc4d-e21f-446f-a4cd-c402f2593e8a", + new_chat_suggested: { + wasSuggested: false, + }, messages: [ { role: "user", diff --git a/src/__tests__/DeleteChat.test.tsx b/src/__tests__/DeleteChat.test.tsx index ae4b79428..01061d9e6 100644 --- a/src/__tests__/DeleteChat.test.tsx +++ b/src/__tests__/DeleteChat.test.tsx @@ -23,6 +23,9 @@ describe("Delete a Chat form history", () => { id: "abc123", model: "foo", tool_use: "quick", + new_chat_suggested: { + wasSuggested: false, + }, createdAt: now, updatedAt: now, read: true, diff --git a/src/__tests__/PinMessages.test.tsx b/src/__tests__/PinMessages.test.tsx deleted file mode 100644 index 6cb05f9eb..000000000 --- a/src/__tests__/PinMessages.test.tsx +++ /dev/null @@ -1,117 +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", - 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/src/__tests__/RestoreChat.test.tsx b/src/__tests__/RestoreChat.test.tsx index e474d8568..26256ff39 100644 --- a/src/__tests__/RestoreChat.test.tsx +++ b/src/__tests__/RestoreChat.test.tsx @@ -46,6 +46,9 @@ describe("Restore Chat from history", () => { { role: "user", content: "test user message", checkpoints: [] }, { role: "assistant", content: "👋" }, ], + new_chat_suggested: { + wasSuggested: false, + }, read: true, }, }, diff --git a/src/app/middleware.ts b/src/app/middleware.ts index 82e28d197..9f3faf2d5 100644 --- a/src/app/middleware.ts +++ b/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/src/app/store.ts b/src/app/store.ts index 8a3a45677..49b48ee80 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -16,12 +16,12 @@ import { promptsApi, toolsApi, commandsApi, - diffApi, pathApi, pingApi, integrationsApi, dockerApi, telemetryApi, + knowledgeApi, } from "../services/refact"; import { smallCloudApi } from "../services/smallcloud"; import { reducer as fimReducer } from "../features/FIM/reducer"; @@ -47,6 +47,8 @@ import { userSurveySlice } from "../features/UserSurvey/userSurveySlice"; import { linksApi } from "../services/refact/links"; import { integrationsSlice } from "../features/Integrations"; import { agentUsageSlice } from "../features/AgentUsage/agentUsageSlice"; +import { currentProjectInfoReducer } from "../features/Chat/currentProject"; +import { knowledgeSlice } from "../features/Knowledge/knowledgeSlice"; import { checkpointsSlice } from "../features/Checkpoints/checkpointsSlice"; import { checkpointsApi } from "../services/refact/checkpoints"; import { patchesAndDiffsTrackerSlice } from "../features/PatchesAndDiffsTracker/patchesAndDiffsTrackerSlice"; @@ -83,6 +85,7 @@ const rootReducer = combineSlices( [agentUsageSlice.reducerPath]: persistedAgentUsageReducer, config: configReducer, active_file: activeFileReducer, + current_project: currentProjectInfoReducer, selected_snippet: selectedSnippetReducer, chat: chatReducer, [statisticsApi.reducerPath]: statisticsApi.reducer, @@ -90,13 +93,13 @@ 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, [linksApi.reducerPath]: linksApi.reducer, [checkpointsApi.reducerPath]: checkpointsApi.reducer, [telemetryApi.reducerPath]: telemetryApi.reducer, + [knowledgeApi.reducerPath]: knowledgeApi.reducer, }, historySlice, errorSlice, @@ -108,6 +111,8 @@ const rootReducer = combineSlices( attachedImagesSlice, userSurveySlice, integrationsSlice, + agentUsageSlice, + knowledgeSlice, checkpointsSlice, patchesAndDiffsTrackerSlice, ); @@ -166,14 +171,13 @@ export function setUpStore(preloadedState?: Partial) { return ( middleware - .concat( + .prepend( pingApi.middleware, statisticsApi.middleware, capsApi.middleware, promptsApi.middleware, toolsApi.middleware, commandsApi.middleware, - diffApi.middleware, smallCloudApi.middleware, pathApi.middleware, linksApi.middleware, @@ -181,6 +185,7 @@ export function setUpStore(preloadedState?: Partial) { dockerApi.middleware, checkpointsApi.middleware, telemetryApi.middleware, + knowledgeApi.middleware, ) .prepend(historyMiddleware.middleware) // .prepend(errorMiddleware.middleware) diff --git a/src/components/Callout/Callout.tsx b/src/components/Callout/Callout.tsx index 473ce8648..0020dd24e 100644 --- a/src/components/Callout/Callout.tsx +++ b/src/components/Callout/Callout.tsx @@ -48,6 +48,7 @@ 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(() => { @@ -86,6 +87,7 @@ export const Callout: React.FC = ({ ); }; +// TODO: Authcall out should not be generic ErrorCallout export const ErrorCallout: React.FC> = ({ timeout = null, onClick, @@ -107,9 +109,9 @@ export const ErrorCallout: React.FC> = ({ {...props} > Error: {children} - {!isAuthError && !preventRetry && ( + {!isAuthError && ( - Click to retry + {preventRetry ? "Click to close" : "Click to retry"} )} {isAuthError && ( diff --git a/src/components/Chat/Chat.stories.tsx b/src/components/Chat/Chat.stories.tsx index 825761b5b..7c79f3487 100644 --- a/src/components/Chat/Chat.stories.tsx +++ b/src/components/Chat/Chat.stories.tsx @@ -6,7 +6,10 @@ import { RootState, setUpStore } from "../../app/store"; import { Provider } from "react-redux"; import { Theme } from "../Theme"; import { AbortControllerProvider } from "../../contexts/AbortControllers"; -import { CHAT_CONFIG_THREAD } from "../../__fixtures__"; +import { + CHAT_CONFIG_THREAD, + CHAT_WITH_KNOWLEDGE_TOOL, +} from "../../__fixtures__"; import { goodCaps, @@ -16,6 +19,8 @@ import { chatLinks, goodTools, noTools, + // noChatLinks, + makeKnowledgeFromChat, } from "../../__fixtures__/msw"; import { TourProvider } from "../../features/Tour"; import { Flex } from "@radix-ui/themes"; @@ -28,6 +33,9 @@ const Template: React.FC<{ id: "test", model: "gpt-4o", // or any model from STUB CAPS REQUEst messages: [], + new_chat_suggested: { + wasSuggested: false, + }, }; const store = setUpStore({ tour: { @@ -115,3 +123,32 @@ export const IDE: Story = { }, }, }; + +export const Knowledge: Story = { + args: { + thread: CHAT_WITH_KNOWLEDGE_TOOL, + config: { + host: "ide", + lspPort: 8001, + themeProps: {}, + features: { + vecdb: true, + knowledge: true, + }, + }, + }, + parameters: { + msw: { + handlers: [ + goodCaps, + goodPing, + goodPrompts, + goodUser, + // noChatLinks, + chatLinks, + noTools, + makeKnowledgeFromChat, + ], + }, + }, +}; diff --git a/src/components/Chat/Chat.tsx b/src/components/Chat/Chat.tsx index 8ad34339d..5f411bd69 100644 --- a/src/components/Chat/Chat.tsx +++ b/src/components/Chat/Chat.tsx @@ -20,6 +20,7 @@ import { selectChatId, selectMessages, getSelectedToolUse, + selectThreadNewChatSuggested, } from "../../features/Chat/Thread"; import { ThreadHistoryButton } from "../Buttons"; import { push } from "../../features/Pages/pagesSlice"; @@ -27,6 +28,7 @@ import { DropzoneProvider } from "../Dropzone"; import { AgentUsage } from "../../features/AgentUsage"; import { useCheckpoints } from "../../hooks/useCheckpoints"; import { Checkpoints } from "../../features/Checkpoints"; +import { SuggestNewChat } from "../ChatForm/SuggestNewChat"; export type ChatProps = { host: Config["host"]; @@ -53,6 +55,8 @@ export const Chat: React.FC = ({ const { submit, abort, retryFromIndex } = useSendChatRequest(); const chatToolUse = useAppSelector(getSelectedToolUse); + + const threadNewChatSuggested = useAppSelector(selectThreadNewChatSuggested); const messages = useAppSelector(selectMessages); const capsForToolUse = useCapsForToolUse(); const { disableInput } = useAgentUsage(); @@ -116,6 +120,12 @@ export const Chat: React.FC = ({ {shouldCheckpointsPopupBeShown && } + {!isStreaming && preventSend && unCalledTools && ( diff --git a/src/components/ChatContent/AssistantInput.tsx b/src/components/ChatContent/AssistantInput.tsx index a27f6633e..6456f4ae1 100644 --- a/src/components/ChatContent/AssistantInput.tsx +++ b/src/components/ChatContent/AssistantInput.tsx @@ -6,15 +6,18 @@ import { ToolCall } from "../../services/refact"; import { ToolContent } from "./ToolsContent"; import { fallbackCopying } from "../../utils/fallbackCopying"; import { telemetryApi } from "../../services/refact/telemetry"; +import { LikeButton } from "./LikeButton"; type ChatInputProps = { message: string | null; toolCalls?: ToolCall[] | null; + isLast?: boolean; }; export const AssistantInput: React.FC = ({ message, toolCalls, + isLast, }) => { const [sendTelemetryEvent] = telemetryApi.useLazySendTelemetryChatEventQuery(); @@ -63,6 +66,7 @@ export const AssistantInput: React.FC = ({ )} {toolCalls && } + {isLast && } ); }; diff --git a/src/components/ChatContent/ChatContent.stories.tsx b/src/components/ChatContent/ChatContent.stories.tsx index f0e2da9cd..7418f0e21 100644 --- a/src/components/ChatContent/ChatContent.stories.tsx +++ b/src/components/ChatContent/ChatContent.stories.tsx @@ -6,7 +6,8 @@ import { setUpStore } from "../../app/store"; import { Theme } from "../Theme"; import { AbortControllerProvider } from "../../contexts/AbortControllers"; import { MarkdownMessage } from "../../__fixtures__/markdown"; -import { ChatMessages, ChatThread } from "../../events"; +import type { ChatMessages } from "../../services/refact"; +import type { ChatThread } from "../../features/Chat/Thread"; import { CHAT_FUNCTIONS_MESSAGES, CHAT_WITH_DIFF_ACTIONS, @@ -16,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; @@ -28,6 +41,9 @@ const MockedStore: React.FC<{ id: "test", model: "test", messages: messages ?? [], + new_chat_suggested: { + wasSuggested: false, + }, }; const store = setUpStore({ chat: { @@ -141,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/src/components/ChatContent/ChatContent.tsx b/src/components/ChatContent/ChatContent.tsx index a6c825ec7..a356341de 100644 --- a/src/components/ChatContent/ChatContent.tsx +++ b/src/components/ChatContent/ChatContent.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useMemo, useRef } from "react"; import { ChatMessages, + isAssistantMessage, isChatContextFileMessage, isDiffMessage, isToolMessage, @@ -181,12 +182,14 @@ function renderMessages( if (head.role === "assistant") { const key = "assistant-input-" + index; + const isLast = !tail.some(isAssistantMessage); const nextMemo = [ ...memo, , ]; diff --git a/src/components/ChatContent/LikeButton.module.css b/src/components/ChatContent/LikeButton.module.css new file mode 100644 index 000000000..094b72b5b --- /dev/null +++ b/src/components/ChatContent/LikeButton.module.css @@ -0,0 +1,20 @@ +.like__button__success { + animation: successAnimation 0.5s ease-in-out; + animation-fill-mode: forwards; +} + +@keyframes successAnimation { + 0% { + transform: scale(1); + color: var(--green-9); + } + 50% { + transform: scale(1.2); + color: var(--yellow-9); + } + 100% { + transform: scale(1); + color: var(--blue-9); + display: none; + } +} diff --git a/src/components/ChatContent/LikeButton.tsx b/src/components/ChatContent/LikeButton.tsx new file mode 100644 index 000000000..c9e9a54df --- /dev/null +++ b/src/components/ChatContent/LikeButton.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { IconButton, Flex } from "@radix-ui/themes"; +import classnames from "classnames"; +import { knowledgeApi } from "../../services/refact/knowledge"; +import { useAppSelector } from "../../hooks"; +import { + selectIsStreaming, + selectIsWaiting, + selectMessages, +} from "../../features/Chat"; +import styles from "./LikeButton.module.css"; +import { useSelector } from "react-redux"; +import { selectKnowledgeFeature } from "../../features/Config/configSlice"; +import { selectThreadProjectOrCurrentProject } from "../../features/Chat/currentProject"; + +function useCreateMemory() { + const messages = useAppSelector(selectMessages); + const isStreaming = useAppSelector(selectIsStreaming); + const isWaiting = useAppSelector(selectIsWaiting); + const knowledgeEnabled = useSelector(selectKnowledgeFeature); + const currentProjectName = useSelector(selectThreadProjectOrCurrentProject); + const [onLike, likeResponse] = + knowledgeApi.useCreateNewMemoryFromMessagesMutation(); + + const submitLike = React.useCallback(() => { + // TODO: how to get the project for the chat? + void onLike({ project: currentProjectName, messages }); + }, [currentProjectName, messages, onLike]); + + const shouldShow = React.useMemo(() => { + if (!knowledgeEnabled) return false; + if (messages.length === 0) return false; + if (isStreaming) return false; + if (isWaiting) return false; + return true; + }, [knowledgeEnabled, messages.length, isStreaming, isWaiting]); + + return { submitLike, likeResponse, shouldShow }; +} + +export const LikeButton = () => { + const { submitLike, likeResponse, shouldShow } = useCreateMemory(); + + if (!shouldShow) return false; + return ( + + + + + + ); +}; + +const ThumbIcon: React.FC = () => { + return ( + + + + ); +}; diff --git a/src/components/ChatContent/ToolsContent.tsx b/src/components/ChatContent/ToolsContent.tsx index 249803894..27ebda6bb 100644 --- a/src/components/ChatContent/ToolsContent.tsx +++ b/src/components/ChatContent/ToolsContent.tsx @@ -1,8 +1,18 @@ -import React from "react"; +import React, { useCallback, useMemo } from "react"; import * as Collapsible from "@radix-ui/react-collapsible"; -import { Container, Flex, Text, Box, Spinner } from "@radix-ui/themes"; +import { + Container, + Flex, + Text, + Box, + Spinner, + IconButton, + Card, + Separator, +} from "@radix-ui/themes"; import { isMultiModalToolResult, + knowledgeApi, MultiModalToolResult, ToolCall, ToolResult, @@ -20,6 +30,11 @@ import { import { ScrollArea } from "../ScrollArea"; import { takeWhile, fenceBackTicks } from "../../utils"; 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; @@ -204,24 +219,46 @@ export type ToolContentProps = { }; export const ToolContent: React.FC = ({ toolCalls }) => { + const features = useAppSelector(selectFeatures); const ids = toolCalls.reduce((acc, cur) => { if (cur.id !== undefined) return [...acc, cur.id]; return acc; }, []); const allToolResults = useAppSelector(selectManyToolResultsByIds(ids)); - return processToolCalls(toolCalls, allToolResults); + return processToolCalls(toolCalls, allToolResults, features); }; function processToolCalls( toolCalls: ToolCall[], toolResults: ToolResult[], + features: RootState["config"]["features"] = {}, processed: React.ReactNode[] = [], ) { if (toolCalls.length === 0) return processed; const [head, ...tail] = toolCalls; const result = toolResults.find((result) => result.tool_call_id === head.id); + // TODO: handle knowledge differently. + // memories are split in content with 🗃️019957b6ff + + if (features.knowledge && result && head.function.name === "knowledge") { + const elem = ( + + ); + 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( @@ -244,7 +281,10 @@ function processToolCalls( toolResults={multiModalToolResults} /> ); - return processToolCalls(nextTail, toolResults, [...processed, elem]); + return processToolCalls(nextTail, toolResults, features, [ + ...processed, + elem, + ]); } const restInTail = takeWhile(tail, (toolCall) => { @@ -261,7 +301,10 @@ function processToolCalls( toolCalls={[head, ...restInTail]} /> ); - return processToolCalls(nextTail, toolResults, [...processed, elem]); + return processToolCalls(nextTail, toolResults, features, [ + ...processed, + elem, + ]); } const MultiModalToolContent: React.FC<{ @@ -451,3 +494,133 @@ const ToolUsageSummary: React.FC<{ ); }; + +// TODO: make this look nicer. +const Knowledge: React.FC<{ toolCall: ToolCall }> = ({ toolCall }) => { + const [open, setOpen] = React.useState(false); + + // TODO: add voting actions + const maybeResult = useAppSelector((state) => + selectToolResultById(state, toolCall.id), + ); + + const argsString = React.useMemo(() => { + return toolCallArgsToString(toolCall.function.arguments); + }, [toolCall.function.arguments]); + + const memories = useMemo(() => { + if (typeof maybeResult?.content !== "string") return []; + return splitMemories(maybeResult.content); + }, [maybeResult?.content]); + + const functionCalled = "```python\n" + name + "(" + argsString + ")\n```"; + + return ( + + + + setOpen((prev) => !prev)}> + + + 📚 Knowledge + + + + + + + + + + + {functionCalled} + + + + + {memories.map((memory) => { + return ( + + ); + })} + + + + + + ); +}; + +const Memory: React.FC<{ id: string; content: string }> = ({ id, content }) => { + const [updateUsage, status] = knowledgeApi.useUpdateMemoryUsageMutation(); + // correct and relevant, + const handleGood = useCallback(() => { + void updateUsage({ memid: id, correct: 1, relevant: 1 }); + }, [id, updateUsage]); + + // TODO: not correct but relevant, and incorrect but relevant + const handleBad = useCallback(() => { + void updateUsage({ memid: id, correct: -1, relevant: -1 }); + }, [id, updateUsage]); + + return ( + + + + + Memory: {id} + + + + {" "} + + + + + + + + {content} + + + ); +}; + +function splitMemories(text: string): { memid: string; content: string }[] { + // Split by 🗃️ and filter out empty strings + const parts = text.split("🗃️").filter((part) => part.trim()); + + return parts.map((part) => { + const newlineIndex = part.indexOf("\n"); + const memid = part.substring(0, newlineIndex); + const content = part.substring(newlineIndex + 1); + + return { + memid, + content, + }; + }); +} diff --git a/src/components/ChatForm/ChatControls.tsx b/src/components/ChatForm/ChatControls.tsx index 467f13bbc..7bf5a0380 100644 --- a/src/components/ChatForm/ChatControls.tsx +++ b/src/components/ChatForm/ChatControls.tsx @@ -204,7 +204,7 @@ export const ReasoningModeSwitch: React.FC = () => { justify="between" width="100%" > - Use a reasoning model for planning + Use a o3-mini reasoning model for planning = ({ 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(); @@ -157,7 +166,6 @@ export const ChatForm: React.FC = ({ const valueIncludingChecks = addCheckboxValuesToInput( trimmedValue, checkboxes, - config.features?.vecdb ?? false, ); setFileInteracted(false); setLineSelectionInteracted(false); @@ -171,7 +179,6 @@ export const ChatForm: React.FC = ({ disableSend, dispatch, checkboxes, - config.features?.vecdb, setFileInteracted, setLineSelectionInteracted, onSubmit, @@ -268,10 +275,10 @@ export const ChatForm: React.FC = ({ setIsSendImmediately, ]); - if (error) { + if (globalError) { return ( - {error} + {globalError} ); } @@ -291,7 +298,7 @@ export const ChatForm: React.FC = ({ } return ( - + {!isOnline && ( Oops, seems that connection was lost... Check your internet connection diff --git a/src/components/ChatForm/SuggestNewChat/SuggestNewChat.module.css b/src/components/ChatForm/SuggestNewChat/SuggestNewChat.module.css new file mode 100644 index 000000000..c56c1f30c --- /dev/null +++ b/src/components/ChatForm/SuggestNewChat/SuggestNewChat.module.css @@ -0,0 +1,14 @@ +.container { + background-color: var(--violet-a2); + border-radius: var(--radius-2); + border: 1px solid var(--violet-a5); + overflow: hidden; + transition: all 0.3s ease-in-out; + transform: translateY(100%); + opacity: 0; +} + +.visible { + transform: translateY(0%); + opacity: 1; +} diff --git a/src/components/ChatForm/SuggestNewChat/SuggestNewChat.tsx b/src/components/ChatForm/SuggestNewChat/SuggestNewChat.tsx new file mode 100644 index 000000000..325310c4e --- /dev/null +++ b/src/components/ChatForm/SuggestNewChat/SuggestNewChat.tsx @@ -0,0 +1,115 @@ +import { Box, Flex, IconButton, Text } from "@radix-ui/themes"; +import { Cross2Icon } from "@radix-ui/react-icons"; +import { useCallback, useEffect, useState } from "react"; + +import { clearPauseReasonsAndHandleToolsStatus } from "../../../features/ToolConfirmation/confirmationSlice"; +import { useAppDispatch, useAppSelector } from "../../../hooks"; +import { popBackTo, push } from "../../../features/Pages/pagesSlice"; +import { telemetryApi } from "../../../services/refact"; +import { + newChatAction, + selectChatId, + setIsNewChatSuggestionRejected, +} from "../../../features/Chat"; + +import { Link } from "../../Link"; + +import styles from "./SuggestNewChat.module.css"; +import classNames from "classnames"; + +type SuggestNewChatProps = { + shouldBeVisible?: boolean; +}; + +export const SuggestNewChat = ({ + shouldBeVisible = false, +}: SuggestNewChatProps) => { + const dispatch = useAppDispatch(); + const chatId = useAppSelector(selectChatId); + const [sendTelemetryEvent] = + telemetryApi.useLazySendTelemetryChatEventQuery(); + const [isRendered, setIsRendered] = useState(shouldBeVisible); + const [isAnimating, setIsAnimating] = useState(false); + + useEffect(() => { + if (shouldBeVisible) { + setIsRendered(true); + // small delay to ensure the initial state is rendered before animation + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setIsAnimating(true); + }); + }); + } else { + setIsAnimating(false); + const timer = setTimeout(() => { + setIsRendered(false); + }, 300); + return () => { + clearTimeout(timer); + }; + } + }, [shouldBeVisible]); + + const handleClose = () => { + dispatch(setIsNewChatSuggestionRejected({ chatId, value: true })); + void sendTelemetryEvent({ + scope: `dismissedNewChatSuggestionWarning`, + success: true, + error_message: "", + }); + }; + + const onCreateNewChat = useCallback(() => { + const actions = [ + newChatAction(), + clearPauseReasonsAndHandleToolsStatus({ + wasInteracted: false, + confirmationStatus: true, + }), + popBackTo({ name: "history" }), + push({ name: "chat" }), + ]; + + actions.forEach((action) => dispatch(action)); + void sendTelemetryEvent({ + scope: `openNewChat`, + success: true, + error_message: "", + }); + }, [dispatch, sendTelemetryEvent]); + + return ( + + + + Tip: Long chats cause you to reach your + usage limits faster. + + + + Start a new chat + + + + + + + + ); +}; diff --git a/src/components/ChatForm/SuggestNewChat/index.ts b/src/components/ChatForm/SuggestNewChat/index.ts new file mode 100644 index 000000000..6ea5a4a0c --- /dev/null +++ b/src/components/ChatForm/SuggestNewChat/index.ts @@ -0,0 +1 @@ +export { SuggestNewChat } from "./SuggestNewChat"; diff --git a/src/components/ChatForm/ToolConfirmation.tsx b/src/components/ChatForm/ToolConfirmation.tsx index ecce0535b..ece33dfd2 100644 --- a/src/components/ChatForm/ToolConfirmation.tsx +++ b/src/components/ChatForm/ToolConfirmation.tsx @@ -1,5 +1,6 @@ import React, { useMemo } from "react"; import { + PATCH_LIKE_FUNCTIONS, useAppDispatch, useAppSelector, useSendChatRequest, @@ -63,7 +64,9 @@ export const ToolConfirmation: React.FC = ({ const types = pauseReasons.map((reason) => reason.type); const toolCallIds = pauseReasons.map((reason) => reason.tool_call_id); - const isPatchConfirmation = commands.some((command) => command === "patch"); + const isPatchConfirmation = commands.some((command) => + PATCH_LIKE_FUNCTIONS.includes(command), + ); const integrationPaths = pauseReasons.map( (reason) => reason.integr_config_path, diff --git a/src/components/ChatForm/useCommandCompletionAndPreviewFiles.ts b/src/components/ChatForm/useCommandCompletionAndPreviewFiles.ts index 7616673fb..c3c37ef39 100644 --- a/src/components/ChatForm/useCommandCompletionAndPreviewFiles.ts +++ b/src/components/ChatForm/useCommandCompletionAndPreviewFiles.ts @@ -1,9 +1,8 @@ import { useState, useEffect, useMemo, useCallback } from "react"; import { useDebounceCallback } from "usehooks-ts"; import { Checkboxes } from "./useCheckBoxes"; -import { useHasCaps, useAppSelector } from "../../hooks"; +import { useHasCaps } from "../../hooks"; import { addCheckboxValuesToInput } from "./utils"; -import { selectVecdb } from "../../features/Config/configSlice"; import { type CommandCompletionResponse, commandsApi, @@ -76,12 +75,10 @@ function useGetCommandPreviewQuery( } function useGetPreviewFiles(query: string, checkboxes: Checkboxes) { - const hasVecdb = useAppSelector(selectVecdb); - const queryWithCheckboxes = useMemo( - () => addCheckboxValuesToInput(query, checkboxes, hasVecdb), + () => addCheckboxValuesToInput(query, checkboxes), // eslint-disable-next-line react-hooks/exhaustive-deps - [checkboxes, query, hasVecdb, checkboxes.file_upload.value], + [checkboxes, query, checkboxes.file_upload.value], ); const [previewQuery, setPreviewQuery] = useState(queryWithCheckboxes); diff --git a/src/components/ChatForm/useInputValue.ts b/src/components/ChatForm/useInputValue.ts index ba2d670ad..e55ad46bb 100644 --- a/src/components/ChatForm/useInputValue.ts +++ b/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/src/components/ChatForm/utils.ts b/src/components/ChatForm/utils.ts index a20d65e52..38a684930 100644 --- a/src/components/ChatForm/utils.ts +++ b/src/components/ChatForm/utils.ts @@ -5,7 +5,6 @@ import type { Checkboxes } from "./useCheckBoxes"; export function addCheckboxValuesToInput( input: string, checkboxes: Checkboxes, - _vecdb: boolean, ) { // prompts go to start let result = input; diff --git a/src/components/ComboBox/ComboBox.test.tsx b/src/components/ComboBox/ComboBox.test.tsx index 3f0edac50..2969885b4 100644 --- a/src/components/ComboBox/ComboBox.test.tsx +++ b/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/src/components/ComboBox/ComboBox.tsx b/src/components/ComboBox/ComboBox.tsx index c4638bef8..c26d013c2 100644 --- a/src/components/ComboBox/ComboBox.tsx +++ b/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/src/components/IntegrationsView/DisplayIntegrations/IntegrationCard.module.css b/src/components/IntegrationsView/DisplayIntegrations/IntegrationCard.module.css index 81f69d5b3..78c7c7659 100644 --- a/src/components/IntegrationsView/DisplayIntegrations/IntegrationCard.module.css +++ b/src/components/IntegrationsView/DisplayIntegrations/IntegrationCard.module.css @@ -3,6 +3,7 @@ width: 100%; cursor: pointer; user-select: none; + transition: opacity 0.15s ease-in-out; } .integrationCardInline { @@ -17,7 +18,20 @@ } } +.disabledCard { + opacity: 0.5; + cursor: not-allowed; +} + .integrationIcon { max-width: 30px; object-fit: cover; } + +.availabilitySwitch { + cursor: pointer; +} + +.disabledAvailabilitySwitch { + cursor: not-allowed; +} diff --git a/src/components/IntegrationsView/DisplayIntegrations/IntegrationCard.tsx b/src/components/IntegrationsView/DisplayIntegrations/IntegrationCard.tsx index ff5f12f2f..e43d18b34 100644 --- a/src/components/IntegrationsView/DisplayIntegrations/IntegrationCard.tsx +++ b/src/components/IntegrationsView/DisplayIntegrations/IntegrationCard.tsx @@ -1,15 +1,20 @@ +import { FC, MouseEventHandler } from "react"; +import classNames from "classnames"; + import { Badge, Card, Flex, Text } from "@radix-ui/themes"; -import styles from "./IntegrationCard.module.css"; +import { useAppSelector } from "../../../hooks"; +import { useUpdateIntegration } from "./useUpdateIntegration"; + import { IntegrationWithIconRecord, NotConfiguredIntegrationWithIconRecord, } from "../../../services/refact"; -import { FC } from "react"; -import classNames from "classnames"; -import { useAppSelector } from "../../../hooks"; + import { selectConfig } from "../../../features/Config/configSlice"; -import { getIntegrationInfo } from "../../../utils/getIntegrationInfo"; import { formatIntegrationIconPath } from "../../../utils/formatIntegrationIconPath"; +import { getIntegrationInfo } from "../../../utils/getIntegrationInfo"; + +import styles from "./IntegrationCard.module.css"; type IntegrationCardProps = { integration: @@ -35,13 +40,35 @@ export const IntegrationCard: FC = ({ const integrationLogo = `http://127.0.0.1:${port}/v1${iconPath}`; const { displayName } = getIntegrationInfo(integration.integr_name); + const { + updateIntegrationAvailability, + integrationAvailability, + isUpdatingAvailability, + } = useUpdateIntegration({ integration }); + + const handleAvailabilityClick: MouseEventHandler = ( + event, + ) => { + if (isUpdatingAvailability) return; + event.stopPropagation(); + void updateIntegrationAvailability(); + }; + + const switches = [ + { label: "On", leftRadius: true }, + { label: "Off", rightRadius: true }, + ]; return ( handleIntegrationShowUp(integration)} + onClick={() => { + if (isUpdatingAvailability) return; + handleIntegrationShowUp(integration); + }} > = ({ {displayName} {!isNotConfigured && ( - - {integration.on_your_laptop ? "On" : "Off"} - + {switches.map(({ label, leftRadius }) => { + const isOn = label === "On"; + const isActive = + isOn === integrationAvailability.on_your_laptop; + + return ( + + {label} + + ); + })} + )} diff --git a/src/components/IntegrationsView/DisplayIntegrations/useUpdateIntegration.ts b/src/components/IntegrationsView/DisplayIntegrations/useUpdateIntegration.ts new file mode 100644 index 000000000..37377f350 --- /dev/null +++ b/src/components/IntegrationsView/DisplayIntegrations/useUpdateIntegration.ts @@ -0,0 +1,88 @@ +import { useCallback, useMemo, useState } from "react"; +import { + areAllFieldsBoolean, + integrationsApi, + IntegrationWithIconRecord, + NotConfiguredIntegrationWithIconRecord, +} from "../../../services/refact"; +import { setError } from "../../../features/Errors/errorsSlice"; +import { useAppDispatch } from "../../../hooks"; + +export const useUpdateIntegration = ({ + integration, +}: { + integration: + | IntegrationWithIconRecord + | NotConfiguredIntegrationWithIconRecord; +}) => { + const dispatch = useAppDispatch(); + + const [getIntegrationData] = + integrationsApi.useLazyGetIntegrationByPathQuery(); + const [saveIntegrationData] = integrationsApi.useSaveIntegrationMutation(); + const [updatedAvailability, setUpdatedAvailability] = useState< + Record + >({ + on_your_laptop: integration.on_your_laptop, + when_isolated: integration.when_isolated, + }); + + const [isUpdatingAvailability, setIsUpdatingAvailability] = useState(false); + + const updateIntegrationAvailability = useCallback(async () => { + if (Array.isArray(integration.integr_config_path)) { + return; + } + + setIsUpdatingAvailability(true); + + const { data: integrationData } = await getIntegrationData( + integration.integr_config_path, + ); + + if (!integrationData?.integr_values) { + return; + } + + const { available } = integrationData.integr_values; + const newAvailability = areAllFieldsBoolean(available) + ? { + on_your_laptop: !available.on_your_laptop, + when_isolated: available.when_isolated, + } + : { + on_your_laptop: integration.on_your_laptop, + when_isolated: integration.when_isolated, + }; + + const response = await saveIntegrationData({ + filePath: integration.integr_config_path, + values: { + ...integrationData.integr_values, + available: newAvailability, + }, + }); + if (response.error) { + dispatch( + setError( + `Error occurred on updating ${integration.integr_name} configuration. Check if your integration configuration is correct`, + ), + ); + setIsUpdatingAvailability(false); + return; + } + + setUpdatedAvailability(newAvailability); + setIsUpdatingAvailability(false); + }, [dispatch, getIntegrationData, saveIntegrationData, integration]); + + const integrationAvailability = useMemo(() => { + return updatedAvailability; + }, [updatedAvailability]); + + return { + updateIntegrationAvailability, + integrationAvailability, + isUpdatingAvailability, + }; +}; diff --git a/src/components/IntegrationsView/IntegrationsTable/EnvironmentVariablesTable.tsx b/src/components/IntegrationsView/IntegrationsTable/EnvironmentVariablesTable.tsx index dedcf1235..7ac98d507 100644 --- a/src/components/IntegrationsView/IntegrationsTable/EnvironmentVariablesTable.tsx +++ b/src/components/IntegrationsView/IntegrationsTable/EnvironmentVariablesTable.tsx @@ -11,7 +11,7 @@ import { DefaultCell } from "./DefaultCell"; import styles from "./ConfirmationTable.module.css"; import { debugIntegrations } from "../../../debugConfig"; -import { MCPEnvs } from "../../../events"; +import { MCPEnvs } from "../../../services/refact"; type EnvironmentVariablesTableProps = { initialData: MCPEnvs; diff --git a/src/components/IntegrationsView/IntegrationsView.tsx b/src/components/IntegrationsView/IntegrationsView.tsx index e7b3826f1..a1f53e0e8 100644 --- a/src/components/IntegrationsView/IntegrationsView.tsx +++ b/src/components/IntegrationsView/IntegrationsView.tsx @@ -200,6 +200,17 @@ export const IntegrationsView: FC = ({ {renderHeader()} {renderContent()} + {globalError && ( + dispatch(clearError())} + className={styles.popup} + preventRetry + > + {globalError} + + )} ); diff --git a/src/components/IntegrationsView/hooks/useIntegrations.ts b/src/components/IntegrationsView/hooks/useIntegrations.ts index daed014f4..db25974f9 100644 --- a/src/components/IntegrationsView/hooks/useIntegrations.ts +++ b/src/components/IntegrationsView/hooks/useIntegrations.ts @@ -394,12 +394,37 @@ export const useIntegrations = ({ currentIntegrationValues ) { setIsDisabledIntegrationForm((isDisabled) => { + const isMCPIntegration = currentIntegration.integr_name.includes("mcp"); const toolParametersChanged = toolParameters && - areToolParameters(currentIntegrationValues.parameters) + areToolParameters(currentIntegrationValues.parameters) && + !isMCPIntegration // if integration is MCP, then not checking toolParameters ? !isEqual(toolParameters, currentIntegrationValues.parameters) : false; + const MCPArgumentsChanged = isMCPArgumentsArray( + currentIntegrationValues.args, + ) + ? !isEqual(currentIntegrationValues.args, MCPArguments) + : false; + + const MCPEnvironmentVariablesChanged = isMCPEnvironmentsDict( + currentIntegrationValues.env, + ) + ? !isEqual(currentIntegrationValues.env, MCPEnvironmentVariables) + : false; + + const confirmationRulesChanged = !isEqual( + confirmationRules, + currentIntegrationValues.confirmation, + ); + + const someFieldsHaveBeenChanged = + confirmationRulesChanged || + toolParametersChanged || + MCPArgumentsChanged || + MCPEnvironmentVariablesChanged; + // Manually collecting data from the form const formElement = document.getElementById( `form-${currentIntegration.integr_name}`, @@ -433,44 +458,22 @@ export const useIntegrations = ({ }, ); - const confirmationRulesChanged = !isEqual( - confirmationRules, - currentIntegrationValues.confirmation, - ); - - debugIntegrations( - `[DEBUG confirmationRulesChanged]: confirmationRulesChanged: `, - confirmationRulesChanged, - ); - - const allToolParametersNamesInSnakeCase = toolParameters - ? toolParameters.every((param) => validateSnakeCase(param.name)) - : true; - - debugIntegrations( - `[DEBUG MCP]: allToolParametersNamesInSnakeCase: `, - allToolParametersNamesInSnakeCase, + const allToolParametersAreWrittenInSnakeCase = toolParameters?.every( + (param) => validateSnakeCase(param.name), ); - if (!allToolParametersNamesInSnakeCase) { - return true; // Disabling form if any of tool parameters names are written not in snake case - } - - if ((toolParametersChanged || confirmationRulesChanged) && isDisabled) { - return false; // Enable form if toolParameters changed and form was disabled - } if ( - otherFieldsChanged && - (toolParametersChanged || confirmationRulesChanged) + typeof allToolParametersAreWrittenInSnakeCase !== "undefined" && + !allToolParametersAreWrittenInSnakeCase ) { - return isDisabled; // Keep the form in the same condition + return true; // Disabling form if any of toolParameters are defined and not written in snake case } - if ( - !otherFieldsChanged && - !toolParametersChanged && - !confirmationRulesChanged - ) { + if (someFieldsHaveBeenChanged && isDisabled) { + return false; + } + + if (!otherFieldsChanged && !someFieldsHaveBeenChanged) { return true; // Disable form if all fields are back to original state } @@ -483,6 +486,8 @@ export const useIntegrations = ({ currentIntegrationSchema, confirmationRules, currentIntegration, + MCPArguments, + MCPEnvironmentVariables, ]); const handleSetCurrentIntegrationSchema = ( @@ -678,7 +683,7 @@ export const useIntegrations = ({ : false; debugIntegrations( - `[DEBUG]: eachFormValueIsNotChanged: `, + `[DEBUG MCP]: eachFormValueIsNotChanged: `, eachFormValueIsNotChanged, ); @@ -719,26 +724,29 @@ export const useIntegrations = ({ areToolConfirmation(currentIntegrationValues.confirmation) ? isEqual(currentIntegrationValues.confirmation, confirmationRules) : true; - debugIntegrations(`[DEBUG]: formValues: `, formValues); + debugIntegrations(`[DEBUG MCP]: formValues: `, formValues); debugIntegrations( - `[DEBUG]: currentIntegrationValues: `, + `[DEBUG MCP]: currentIntegrationValues: `, currentIntegrationValues, ); debugIntegrations( - `[DEBUG]: eachAvailabilityOptionIsNotChanged: `, + `[DEBUG MCP]: eachAvailabilityOptionIsNotChanged: `, eachAvailabilityOptionIsNotChanged, ); debugIntegrations( - `[DEBUG]: eachToolParameterIsNotChanged: `, + `[DEBUG MCP]: eachToolParameterIsNotChanged: `, eachToolParameterIsNotChanged, ); debugIntegrations( - `[DEBUG]: eachToolConfirmationIsNotChanged: `, + `[DEBUG MCP]: eachToolConfirmationIsNotChanged: `, eachToolConfirmationIsNotChanged, ); - debugIntegrations(`[DEBUG]: availabilityValues: `, availabilityValues); + debugIntegrations( + `[DEBUG MCP]: availabilityValues: `, + availabilityValues, + ); const maybeDisabled = eachFormValueIsNotChanged && eachAvailabilityOptionIsNotChanged && @@ -747,22 +755,27 @@ export const useIntegrations = ({ eachMCPArgumentIsNotChanged && eachMCPEnvironmentVariableIsNotChanged; - debugIntegrations(`[DEBUG CHANGE]: maybeDisabled: `, maybeDisabled); + debugIntegrations(`[DEBUG MCP]: maybeDisabled: `, maybeDisabled); - const areAllCommandsWrittenInSnakeCase = - toolParameters?.every((param) => validateSnakeCase(param.name)) && - MCPArguments.every((arg) => validateSnakeCase(arg)) && - Object.entries(MCPEnvironmentVariables).every(([key]) => - validateSnakeCase(key), - ); + const areToolParametersWrittenInSnakeCase = toolParameters?.every( + (param) => validateSnakeCase(param.name), + ); - setIsDisabledIntegrationForm( - areAllCommandsWrittenInSnakeCase !== undefined - ? areAllCommandsWrittenInSnakeCase + debugIntegrations( + `[DEBUG MCP]: areToolParametersWrittenInSnakeCase: `, + areToolParametersWrittenInSnakeCase, + ); + + const newDisabled = + areToolParametersWrittenInSnakeCase !== undefined + ? areToolParametersWrittenInSnakeCase ? maybeDisabled : true - : maybeDisabled, - ); + : maybeDisabled; + + debugIntegrations(`[DEBUG MCP]: newDisabled: `, newDisabled); + + setIsDisabledIntegrationForm(newDisabled); }, [ currentIntegration, diff --git a/src/components/Markdown/CodeBlock.tsx b/src/components/Markdown/CodeBlock.tsx index fd7e107f1..f4013b97f 100644 --- a/src/components/Markdown/CodeBlock.tsx +++ b/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/src/components/Markdown/Markdown.tsx b/src/components/Markdown/Markdown.tsx index c5861c559..2c338a07a 100644 --- a/src/components/Markdown/Markdown.tsx +++ b/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/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index f4d52d965..82fdafa67 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -6,6 +6,7 @@ import { useAppSelector, useAppDispatch } from "../../hooks"; import { deleteChatById } from "../../features/History/historySlice"; import { push } from "../../features/Pages/pagesSlice"; import { restoreChat, type ChatThread } from "../../features/Chat/Thread"; +import { FeatureMenu } from "../../features/Config/FeatureMenu"; export type SidebarProps = { takingNotes: boolean; @@ -43,6 +44,7 @@ export const Sidebar: React.FC = ({ takingNotes, style }) => { return ( + diff --git a/src/components/TextArea/TextArea.tsx b/src/components/TextArea/TextArea.tsx index e51dda08c..939d76eb1 100644 --- a/src/components/TextArea/TextArea.tsx +++ b/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/src/components/Toolbar/Dropdown.tsx b/src/components/Toolbar/Dropdown.tsx index 9bb02e1e9..6da9184ac 100644 --- a/src/components/Toolbar/Dropdown.tsx +++ b/src/components/Toolbar/Dropdown.tsx @@ -1,5 +1,9 @@ import React, { useCallback, useMemo } from "react"; -import { selectHost, type Config } from "../../features/Config/configSlice"; +import { + selectHost, + selectKnowledgeFeature, + type Config, +} from "../../features/Config/configSlice"; import { useTourRefs } from "../../features/Tour"; import { useConfig, @@ -14,6 +18,7 @@ import { useOpenUrl } from "../../hooks/useOpenUrl"; import { Button, DropdownMenu, Flex, IconButton } from "@radix-ui/themes"; import { HamburgerMenuIcon, DiscordLogoIcon } from "@radix-ui/react-icons"; import { clearHistory } from "../../features/History/historySlice"; +import { KnowledgeListPage } from "../../features/Pages/pagesSlice"; //import { Coin } from "../../images"; export type DropdownNavigationOptions = @@ -24,6 +29,7 @@ export type DropdownNavigationOptions = | "restart tour" | "login page" | "integrations" + | KnowledgeListPage["name"] | ""; type DropdownProps = { @@ -61,6 +67,7 @@ export const Dropdown: React.FC = ({ const dispatch = useAppDispatch(); const logout = useLogout(); const { addressURL } = useConfig(); + const knowledgeEnabled = useAppSelector(selectKnowledgeFeature); const { startPollingForUser } = useAgentUsage(); const bugUrl = linkForBugReports(host); @@ -152,9 +159,17 @@ export const Dropdown: React.FC = ({ handleNavigation("integrations")}> - Set up Agent Integrations + Setup Agent Integrations + {knowledgeEnabled && ( + handleNavigation("knowledge list")} + > + Manage Knowledge + + )} + handleNavigation("settings")}> {refactProductType} Settings diff --git a/src/components/Toolbar/Toolbar.tsx b/src/components/Toolbar/Toolbar.tsx index 65c5cf8a5..df7b1a81b 100644 --- a/src/components/Toolbar/Toolbar.tsx +++ b/src/components/Toolbar/Toolbar.tsx @@ -144,6 +144,9 @@ export const Toolbar = ({ activeTab }: ToolbarProps) => { } else if (to === "chat") { dispatch(popBackTo({ name: "history" })); dispatch(push({ name: "chat" })); + } else if (to === "knowledge list") { + // TODO: send telemetry + dispatch(push({ name: "knowledge list" })); } }, [dispatch, sendTelemetryEvent, openSettings, openHotKeys], diff --git a/src/components/Tools/Texdoc.module.css b/src/components/Tools/Texdoc.module.css new file mode 100644 index 000000000..fb6fabb2d --- /dev/null +++ b/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/src/components/Tools/Textdoc.tsx b/src/components/Tools/Textdoc.tsx new file mode 100644 index 000000000..17b63209a --- /dev/null +++ b/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/src/components/Tools/types.ts b/src/components/Tools/types.ts new file mode 100644 index 000000000..17a073814 --- /dev/null +++ b/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/src/events/index.ts b/src/events/index.ts index f9956c5d6..0f3de6e46 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -17,7 +17,7 @@ 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"; export { type FileInfo, setFileInfo } from "../features/Chat/activeFile"; @@ -28,9 +28,21 @@ 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, +} from "../features/Chat/currentProject"; -export const showPatchTicket = createAction("showPatchTicket"); +export type { + ToolCommand, + CustomPromptsResponse, + CapsResponse, + UserMessage, + ChatMessage, + ChatMessages, + DiffChunk, + ToolEditResult, +} from "../services/refact"; // TODO: re-exporting from redux seems to break things :/ export type InitialState = { @@ -44,24 +56,24 @@ export type InitialState = { history: HistoryState; error: ErrorSliceState; pages: PageSliceState; + current_project: CurrentProjectInfo; }; export { ideOpenFile, type OpenFilePayload, ideDiffPasteBackAction, - ideDiffPreviewAction, ideNewFileAction, ideOpenHotKeys, ideOpenSettingsAction, ideOpenChatInNewTab, ideAnimateFileStart, ideAnimateFileStop, - ideWriteResultsToFile, ideChatPageChange, ideEscapeKeyPressed, ideIsChatStreaming, ideIsChatReady, + ideToolEdit, } from "../hooks/useEventBusForIDE"; export const fim = { @@ -96,7 +108,7 @@ export { isUserMessage, } from "../services/refact"; -export type * from "../services/refact"; +// export type * from "../services/refact"; export * from "./setup"; export type * from "./setup"; diff --git a/src/features/App.tsx b/src/features/App.tsx index 6d075d124..aac76d736 100644 --- a/src/features/App.tsx +++ b/src/features/App.tsx @@ -30,6 +30,7 @@ import { ThreadHistory } from "./ThreadHistory"; import { Integrations } from "./Integrations"; import { UserSurvey } from "./UserSurvey"; import { integrationsApi } from "../services/refact"; +import { KnowledgeList } from "./Knowledge"; import { LoginPage } from "./Login"; import styles from "./App.module.css"; @@ -210,6 +211,7 @@ export const InnerApp: React.FC = ({ style }: AppProps) => { chatId={page.chatId} /> )} + {page.name === "knowledge list" && } {page.name !== "welcome" && } diff --git a/src/features/Chat/Thread/actions.ts b/src/features/Chat/Thread/actions.ts index f6081d0f1..fe60a2464 100644 --- a/src/features/Chat/Thread/actions.ts +++ b/src/features/Chat/Thread/actions.ts @@ -7,6 +7,7 @@ import { IntegrationMeta, LspChatMode, PayloadWithChatAndMessageId, + PayloadWithChatAndBoolean, } from "./types"; import { isAssistantDelta, @@ -56,6 +57,15 @@ export const setLastUserMessageId = createAction( "chatThread/setLastUserMessageId", ); +export const setIsNewChatSuggested = createAction( + "chatThread/setIsNewChatSuggested", +); + +export const setIsNewChatSuggestionRejected = + createAction( + "chatThread/setIsNewChatSuggestionRejected", + ); + export const backUpMessages = createAction< PayloadWithId & { messages: ChatThread["messages"]; diff --git a/src/features/Chat/Thread/reducer.ts b/src/features/Chat/Thread/reducer.ts index 2cf10b09f..04c287cb1 100644 --- a/src/features/Chat/Thread/reducer.ts +++ b/src/features/Chat/Thread/reducer.ts @@ -33,6 +33,8 @@ import { setLastUserMessageId, setEnabledCheckpoints, fixBrokenToolMessages, + setIsNewChatSuggested, + setIsNewChatSuggestionRejected, } from "./actions"; import { formatChatResponse } from "./utils"; import { @@ -55,6 +57,9 @@ const createChatThread = ( tool_use, integration, mode, + new_chat_suggested: { + wasSuggested: false, + }, }; return chat; }; @@ -198,6 +203,21 @@ export const chatReducer = createReducer(initialState, (builder) => { state.automatic_patch = action.payload; }); + builder.addCase(setIsNewChatSuggested, (state, action) => { + if (state.thread.id !== action.payload.chatId) return state; + state.thread.new_chat_suggested = { + wasSuggested: action.payload.value, + }; + }); + + builder.addCase(setIsNewChatSuggestionRejected, (state, action) => { + if (state.thread.id !== action.payload.chatId) return state; + state.thread.new_chat_suggested = { + ...state.thread.new_chat_suggested, + wasRejectedByUser: action.payload.value, + }; + }); + builder.addCase(setEnabledCheckpoints, (state, action) => { state.checkpoints_enabled = action.payload; }); diff --git a/src/features/Chat/Thread/selectors.ts b/src/features/Chat/Thread/selectors.ts index abdcb6fb4..2f1485680 100644 --- a/src/features/Chat/Thread/selectors.ts +++ b/src/features/Chat/Thread/selectors.ts @@ -15,6 +15,9 @@ export const selectAutomaticPatch = (state: RootState) => export const selectCheckpointsEnabled = (state: RootState) => state.chat.checkpoints_enabled; + +export const selectThreadNewChatSuggested = (state: RootState) => + state.chat.thread.new_chat_suggested; export const selectIsWaiting = (state: RootState) => state.chat.waiting_for_response; export const selectIsStreaming = (state: RootState) => state.chat.streaming; diff --git a/src/features/Chat/Thread/types.ts b/src/features/Chat/Thread/types.ts index 588c8697e..10af4e927 100644 --- a/src/features/Chat/Thread/types.ts +++ b/src/features/Chat/Thread/types.ts @@ -20,7 +20,14 @@ export type ChatThread = { isTitleGenerated?: boolean; integration?: IntegrationMeta | null; mode?: LspChatMode; + project_name?: string; last_user_message_id?: string; + new_chat_suggested: SuggestedChat; +}; + +export type SuggestedChat = { + wasSuggested: boolean; + wasRejectedByUser?: boolean; }; export type ToolUse = "quick" | "explore" | "agent"; @@ -42,6 +49,7 @@ export type Chat = { export type PayloadWithId = { id: string }; export type PayloadWithChatAndMessageId = { chatId: string; messageId: string }; +export type PayloadWithChatAndBoolean = { chatId: string; value: boolean }; export type PayloadWithIdAndTitle = { title: string; isTitleGenerated: boolean; @@ -79,7 +87,10 @@ export function chatModeToLspMode( toolUse?: ToolUse, mode?: LspChatMode, ): LspChatMode { - if (mode) return mode; + if (mode) { + if (mode === "AGENT" || mode === "THINKING_AGENT") return "AGENT"; + return mode; + } if (toolUse === "agent") return "AGENT"; if (toolUse === "quick") return "NO_TOOLS"; return "EXPLORE"; diff --git a/src/features/Chat/activeFile.ts b/src/features/Chat/activeFile.ts index df9fbcec3..43bc65331 100644 --- a/src/features/Chat/activeFile.ts +++ b/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/src/features/Chat/currentProject.ts b/src/features/Chat/currentProject.ts new file mode 100644 index 000000000..39c74e05c --- /dev/null +++ b/src/features/Chat/currentProject.ts @@ -0,0 +1,31 @@ +import { createReducer, createAction } from "@reduxjs/toolkit"; +import { RootState } from "../../app/store"; + +export type CurrentProjectInfo = { + name: string; +}; + +const initialState: CurrentProjectInfo = { + name: "", +}; + +export const setCurrentProjectInfo = createAction( + "currentProjectInfo/setCurrentProjectInfo", +); + +export const currentProjectInfoReducer = createReducer( + initialState, + (builder) => { + builder.addCase(setCurrentProjectInfo, (_state, action) => { + // state.name = action.payload.name; + return action.payload; + }); + }, +); + +export const selectThreadProjectOrCurrentProject = (state: RootState) => { + if (state.chat.thread.integration?.project) { + return state.chat.thread.integration.project; + } + return state.chat.thread.project_name ?? state.current_project.name; +}; diff --git a/src/features/Config/FeatureMenu.tsx b/src/features/Config/FeatureMenu.tsx new file mode 100644 index 000000000..16f2b08ba --- /dev/null +++ b/src/features/Config/FeatureMenu.tsx @@ -0,0 +1,127 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { Dialog, Flex, Button, Checkbox, Text } from "@radix-ui/themes"; +import { + useAppDispatch, + useAppSelector, + useEventsBusForIDE, +} from "../../hooks"; +import { selectFeatures, changeFeature } from "./configSlice"; +import { Link } from "../../components/Link"; + +const useInputEvent = () => { + const [key, setKey] = useState(null); + useEffect(() => { + const keyDownHandler = (event: KeyboardEvent) => setKey(event.code); + const keyUpHandler = () => setKey(null); + window.addEventListener("keydown", keyDownHandler); + window.addEventListener("keyup", keyUpHandler); + return () => { + window.removeEventListener("keydown", keyDownHandler); + window.removeEventListener("keyup", keyUpHandler); + }; + }, []); + + return key; +}; + +const konamiCode = [ + "ArrowUp", + "ArrowUp", + "ArrowDown", + "ArrowDown", + "ArrowLeft", + "ArrowRight", + "ArrowLeft", + "ArrowRight", + "Escape", + "Enter", +]; + +const useKonamiCode = () => { + const [count, setCount] = useState(0); + const [success, setSuccess] = useState(false); + const key = useInputEvent(); + + useEffect(() => { + if (success) { + return; + } else if (document.activeElement !== document.body) { + return; + } else if (count === konamiCode.length) { + setSuccess(true); + } else if (key === konamiCode[count]) { + setCount((n) => n + 1); + } + }, [key, count, success]); + + const reset = useCallback(() => { + setSuccess(false); + setCount(0); + }, []); + + return { success, reset }; +}; + +export const FeatureMenu: React.FC = () => { + const { success, reset } = useKonamiCode(); + const dispatch = useAppDispatch(); + const features = useAppSelector(selectFeatures); + + const { openSettings } = useEventsBusForIDE(); + + const handleSettingsClick = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + openSettings(); + }, + [openSettings], + ); + + // if (!success) return false; + + const keysAndValues = Object.entries(features ?? {}); + + return ( + + + Hidden Features Menu + {keysAndValues.length === 0 && ( + No hidden features + )} + + {keysAndValues.map(([feature, value]) => { + const setInSettings = feature === "ast" || feature === "vecdb"; + return ( + + + { + dispatch(changeFeature({ feature, value: !value })); + }} + disabled={setInSettings} + />{" "} + {feature} + {setInSettings && ( + + Option set in{" "} + settings + + )} + + + ); + })} + + + + + + + + + + ); +}; diff --git a/src/features/Config/configSlice.ts b/src/features/Config/configSlice.ts index 866835b6c..a481766a0 100644 --- a/src/features/Config/configSlice.ts +++ b/src/features/Config/configSlice.ts @@ -1,5 +1,4 @@ -import { createReducer } from "@reduxjs/toolkit"; -import { createAction } from "@reduxjs/toolkit"; +import { createReducer, createAction, createSelector } from "@reduxjs/toolkit"; import { type ThemeProps } from "../../components/Theme"; import { RootState } from "../../app/store"; @@ -16,6 +15,7 @@ export type Config = { vecdb?: boolean; ast?: boolean; images?: boolean; + knowledge?: boolean; }; keyBindings?: { completeManual?: string; @@ -34,6 +34,7 @@ const initialState: Config = { vecdb: true, ast: true, images: true, + knowledge: false, }, themeProps: { appearance: "dark", @@ -48,6 +49,11 @@ export const setThemeMode = createAction<"light" | "dark" | "inherit">( export const setApiKey = createAction("config/setApiKey"); export const setAddressURL = createAction("config/setAddressURL"); +export const changeFeature = createAction<{ + feature: string; + value: boolean; +}>("config/feature/change"); + export const reducer = createReducer(initialState, (builder) => { // TODO: toggle darkmode for web host? builder.addCase(updateConfig, (state, action) => { @@ -77,6 +83,13 @@ export const reducer = createReducer(initialState, (builder) => { state.apiKey = action.payload; }); + builder.addCase(changeFeature, (state, action) => { + state.features = { + ...(state.features ?? {}), + [action.payload.feature]: action.payload.value, + }; + }); + builder.addCase(setAddressURL, (state, action) => { state.addressURL = action.payload; }); @@ -87,10 +100,20 @@ export const selectThemeMode = (state: RootState) => export const selectConfig = (state: RootState) => state.config; export const selectLspPort = (state: RootState) => state.config.lspPort; -export const selectVecdb = (state: RootState) => - state.config.features?.vecdb ?? false; -export const selectAst = (state: RootState) => - state.config.features?.ast ?? false; + +export const selectFeatures = (state: RootState) => state.config.features; +export const selectVecdb = createSelector( + selectFeatures, + (features) => features?.vecdb, +); +export const selectAst = createSelector( + selectFeatures, + (features) => features?.ast, +); +export const selectKnowledgeFeature = createSelector( + selectFeatures, + (features) => features?.knowledge, +); export const selectApiKey = (state: RootState) => state.config.apiKey; export const selectAddressURL = (state: RootState) => state.config.addressURL; diff --git a/src/features/History/historySlice.ts b/src/features/History/historySlice.ts index 0bee550ff..ff75a7a1d 100644 --- a/src/features/History/historySlice.ts +++ b/src/features/History/historySlice.ts @@ -250,9 +250,14 @@ startHistoryListening({ startHistoryListening({ actionCreator: backUpMessages, effect: (action, listenerApi) => { - const thread = listenerApi.getState().chat.thread; + const state = listenerApi.getState(); + const thread = state.chat.thread; if (thread.id !== action.payload.id) return; - const toSave = { ...thread, messages: action.payload.messages }; + const toSave = { + ...thread, + messages: action.payload.messages, + project_name: thread.project_name ?? state.current_project.name, + }; listenerApi.dispatch(saveChat(toSave)); }, }); diff --git a/src/features/Knowledge/KnowledgeForms.module.css b/src/features/Knowledge/KnowledgeForms.module.css new file mode 100644 index 000000000..5c39d15f4 --- /dev/null +++ b/src/features/Knowledge/KnowledgeForms.module.css @@ -0,0 +1,12 @@ +.knowledge__form { + animation: rollin_from_side 0.2s ease-out; +} + +@keyframes rollin_from_side { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(0); + } +} diff --git a/src/features/Knowledge/KnowledgeForms.tsx b/src/features/Knowledge/KnowledgeForms.tsx new file mode 100644 index 000000000..46c69f2f2 --- /dev/null +++ b/src/features/Knowledge/KnowledgeForms.tsx @@ -0,0 +1,177 @@ +import React, { useCallback, useEffect } from "react"; +import { + Card, + Flex, + Text, + Button, + TextField, + TextArea, + TextAreaProps, + Heading, +} from "@radix-ui/themes"; +import { + isAddMemoryRequest, + isMemUpdateRequest, + knowledgeApi, + MemoRecord, + MemUpdateRequest, +} from "../../services/refact"; +import styles from "./KnowledgeForms.module.css"; + +type EditKnowledgeFormProps = { + memory: MemoRecord; + onClose: () => void; +}; +// TODO: this is similar to Add memory. +export const EditKnowledgeForm: React.FC = ({ + memory, + onClose, +}) => { + const [submit, result] = knowledgeApi.useUpdateMemoryMutation(); + + const handleSubmit = useCallback( + (event: React.FormEvent) => { + event.preventDefault(); + const formData = Object.fromEntries(new FormData(event.currentTarget)); + const oldData: MemUpdateRequest = { + memid: memory.memid, + mem_type: memory.m_type, + goal: memory.m_goal, + project: memory.m_goal, + payload: memory.m_payload, + origin: memory.m_origin, + }; + const updatedMemory = { ...oldData, ...formData }; + // TODO: handle errors + if (isMemUpdateRequest(updatedMemory)) { + void submit(updatedMemory); + } + }, + [memory, submit], + ); + + useEffect(() => { + if (result.isSuccess) { + onClose(); + } + }, [onClose, result.isSuccess]); + + return ( + +
+ Edit a memory + + + + + + + + + + + + +
+
+ ); +}; + +// TODO: for adding, will change slightly +export const AddKnowledgeForm: React.FC<{ onClose: () => void }> = ({ + onClose, +}) => { + const [submit, result] = knowledgeApi.useAddMemoryMutation(); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const memory = Object.fromEntries(formData.entries()); + + if (isAddMemoryRequest(memory)) { + // TODO: handle errors + submit(memory) + .unwrap() + .then(() => { + event.currentTarget.reset(); + }) + .catch(() => ({})); + } + }; + + useEffect(() => { + if (result.isSuccess) { + onClose(); + } + }, [result.isSuccess, onClose]); + + return ( + +
+ Add a new memory + + + + + + + + + + + + +
+
+ ); +}; + +const TextInput: React.FC = ({ + label, + ...props +}) => { + return ( + + {label} + + + ); +}; + +const TextAreaInput: React.FC = ({ + label, + ...props +}) => { + return ( + + {label} +