From a5971d1087c84b9e1a80c46d89c911312e55f9e8 Mon Sep 17 00:00:00 2001 From: Marc McIntosh Date: Tue, 11 Feb 2025 10:20:05 +0100 Subject: [PATCH 01/30] Knowledge db (#196) * wip: thread stream. * wip(chat-db): add middleware to connect to db and get threads. * fix(jb images): remove "add image" button from jetbrains. (#211) * fix(jb images): remove "add image" button from jetbrains. * fix(jb images): make `images` a configurable feature. * wip(knowledge): add sub end point. * wip(knowlage db): handle insert, update and delete operations. * wip(memories): add a basic ui for rendering memories. * wip(knowledge): add create, delete and search services. * wip(knowledge): add url for updating usage. * wip(knowledge): add the knowledge page to the sidebar. * wip(knowledge): add a basic form for adding knowledge. * wip(knowledge form): add back button. * wip(knowledge): add back in listAll and add a scroll area to memories list. * wip(knowledge): list all and subscribe * wip(knowledge): add delete functionality. * ui(knowledge): change the orientation of the data-list depending on size * wip(knowledge list): remove properties the user doesn't need to see from the data list. * ui(knowledge list): remove data list and use `m_goal` as the title and `payload` as content. * feat(knowledge): add a like button. * ui(thumb): change like button from tick to thumb. * refactor(knowledge): remove project, type and origin from add knowledge form * refactor(knowledge): remove `mem-list` endpoint. * feat(knowledge): hide behind a feature flag. * feat(feature menu): add a hidden feature menu. * wip(knowledge): add the ability to edit knowledge. * fix(feature menu): only allow the code to be entered when nothing if focused. * wip(knowledge search): add comments and rough search handler * feat(knowledge): add quick search and fix features usage * fix(features): add links to setting for features set in settings. * chore*knowledge): fix linter warnings * wip(knowledge): add `mem-upd` handler for updating memroies. * wip(knowledge): add status to subscription payload. * wip(knowledge): add vectorisation status information. * wip(knowledge): animate db button while parsing. * wip(knowledge): debouce search and cache vecdb status. * refactor(knowledge): move components to separate files. * ui(knowledge): add titles to the forms. * ui(knowledge): add padding to items so the scroll bar isn't over the boarder * linter: allow log for unsupproted type in knowledge stream. * fix(knowledge): don't wait for consumeStream to resolve, this allows the stream to hangup. * fix: refetch button if counter state is disaligned on frontend with inference * fix: user.refetch instead of reset of API state * fix: updating state of agent usage on refetch of user * fix: void expression for async refetchUser * 2.0.3-alpha.25 * 2.0.3 * feat(knowledge): create knowledge from chat. * wip(knowledge): add a reducer and event listener for the current project name to pass to "/v1/trajectory-save". * wip(knowledge): export active_project types for vscode. * fix(knowledge): if the chat is an integration use the project from the integration data. * wip(knowledge): add a story with a knowledge tool call. * wip(knowledge): add basic ui for updating knowledge useage. * fix(knowledge): hang up on unmount. * wip(knowledge): move subscribe to a thunk so it can hangup requests. * 2.0.4-alpha.0 * fix(caps): refetch caps on mount or arg change. (#307) * fix: unified limit exhausted message across app & adjusted text * fix: better wording * 2.0.4-alpha.1 * fix: updated offline message * fix: increased size for input field * fix: if only one chat tab left and active tab is dashboard, allowing click on chat tab * feat: upgrade to pro button in main dropdown & reworked links in dropdown * fix: show plugin settings if jetbrains, otherwise extension settings * chore: revert deletion of integrations * chore: revert deletion of integrations * fix: startup keyword shouldn't be null * wip(updated tour): integrations step & updated refs * feat: updated onboarding with new data & small adjustments to the UI * 2.0.4-alpha.2 * wip(checkpoints): ui for popup and user input * wip(checkpoints): truncation of links for files * wip(checkpoints): storing checkpoints for user messages * wip(checkpoints): API handler, typeguards, saving restore results to store and fixed fixtures * wip(checkpoints): fixed types issue * wip(checkpoints): fix of tests * wip(checkpoints): stubs for checkpoints & storybook * feat(checkpoints): undo functionality & refactoring * chore: fixed types for CI * refactoring(checkpoints): extracted logic and functions to useCheckpoints hook * chore: parsing error text adjusted * fix: make checkpoints optional & hiding revert changes button if there are no checkpoints * fix: support esc key press & click out of the dialog to close it * chore: removed convertUTCToLocalTime() util * fix: remove hardcoded locale en-GB to use system one * feat: reworked logic & improved UI/UX * chore: better wording with dates * fix: checking for index of of current message instead of checking for user messages amount * fix(onboarding): updated GIF and links for final screen * fix: updated tour ending * 2.0.4-alpha.3 * fix: fixed validation of paths for puzzle links on Windows, adjusted texts * improve(puzzle_links): extractLinkFromPuzzle unit tests coverage * fix: tour blinking on restart & typos & unified titles for buttons & removed conditions for set up integrations button * 2.0.4-alpha.4 * 2.0.4 * 2.0.5-alpha.0 * Feat: Agent Capabilities (#321) * feat: better controls for agentic features * wip(chat-db): add knowledge sub thunk. * fix: clear memery when make a new request * wip(knowledge): move search to a thunk. * Login rework (#320) * 2.0.4-alpha.0 * fix(caps): refetch caps on mount or arg change. (#307) * fix: unified limit exhausted message across app & adjusted text * fix: better wording * 2.0.4-alpha.1 * fix: updated offline message * fix: increased size for input field * fix: if only one chat tab left and active tab is dashboard, allowing click on chat tab * feat: upgrade to pro button in main dropdown & reworked links in dropdown * fix: show plugin settings if jetbrains, otherwise extension settings * chore: revert deletion of integrations * chore: revert deletion of integrations * fix: startup keyword shouldn't be null * wip(updated tour): integrations step & updated refs * feat: updated onboarding with new data & small adjustments to the UI * 2.0.4-alpha.2 * wip(login): add un-themed accordion. * wip(accordion): light mode style * wip(accordion): support light and dark mode. * ui(accordion): use theme colour. * wip(login): add basic ui for login page. * ui(login): add padding to the container for the login page. * wip(login): add msw handlers for story book. * wip(checkpoints): ui for popup and user input * wip(checkpoints): truncation of links for files * wip(checkpoints): storing checkpoints for user messages * wip(checkpoints): API handler, typeguards, saving restore results to store and fixed fixtures * wip(checkpoints): fixed types issue * wip(checkpoints): fix of tests * wip(checkpoints): stubs for checkpoints & storybook * feat(checkpoints): undo functionality & refactoring * chore: fixed types for CI * refactoring(checkpoints): extracted logic and functions to useCheckpoints hook * chore: parsing error text adjusted * fix: make checkpoints optional & hiding revert changes button if there are no checkpoints * fix: support esc key press & click out of the dialog to close it * chore: removed convertUTCToLocalTime() util * fix: remove hardcoded locale en-GB to use system one * feat: reworked logic & improved UI/UX * chore: better wording with dates * fix: checking for index of of current message instead of checking for user messages amount * fix(onboarding): updated GIF and links for final screen * fix: updated tour ending * 2.0.4-alpha.3 * wip(email login): add hooks and msw interception for storybook * ui(login): small ui adjustments. * wip(login): handle canceling email polling. * fix(email login): remember to set api key on success. * wip(login): send setup host event to ide. * fix(email login redirect): quick fix, will need to look closer at App.tsx routing. * fix: fixed validation of paths for puzzle links on Windows, adjusted texts * improve(puzzle_links): extractLinkFromPuzzle unit tests coverage * fix: tour blinking on restart & typos & unified titles for buttons & removed conditions for set up integrations button * wip(login): remove old login pages. * feat: better controls for agentic features * test(start enw chat): for some reason stubResizeObserver wasn't being called :/ * wip(login): add provider icons. * refactor(agent usage): move usage update to getUser hook so email login updates the usage. * ui(login): add title. * fix(email message): add missing hooks. * fix(console.log): remove console log. * ui(login): fix ui jumping when changing text. * ui(login): allow all items to be collapsed. * ui(login): remove text changes because of jumps. --------- Co-authored-by: alashchev17 * refactor: remove old chat-db code. * fix: preventing user from re-navigation, if history, welcome or chat pages are already in pages history * Feature: Deepseek Reasoning support (#312) * 2.0.4-alpha.0 * fix(caps): refetch caps on mount or arg change. (#307) * fix: unified limit exhausted message across app & adjusted text * fix: better wording * 2.0.4-alpha.1 * fix: updated offline message * fix: increased size for input field * fix: if only one chat tab left and active tab is dashboard, allowing click on chat tab * feat: upgrade to pro button in main dropdown & reworked links in dropdown * fix: show plugin settings if jetbrains, otherwise extension settings * chore: revert deletion of integrations * chore: revert deletion of integrations * fix: startup keyword shouldn't be null * wip(updated tour): integrations step & updated refs * feat: updated onboarding with new data & small adjustments to the UI * 2.0.4-alpha.2 * feat: THINKING_AGENT mode support for deepseek-reasoning model * wip(checkpoints): ui for popup and user input * wip(checkpoints): truncation of links for files * wip(checkpoints): storing checkpoints for user messages * wip(checkpoints): API handler, typeguards, saving restore results to store and fixed fixtures * wip(checkpoints): fixed types issue * wip(checkpoints): fix of tests * wip(checkpoints): stubs for checkpoints & storybook * feat(checkpoints): undo functionality & refactoring * chore: fixed types for CI * refactoring(checkpoints): extracted logic and functions to useCheckpoints hook * chore: parsing error text adjusted * fix: make checkpoints optional & hiding revert changes button if there are no checkpoints * fix: support esc key press & click out of the dialog to close it * chore: removed convertUTCToLocalTime() util * fix: remove hardcoded locale en-GB to use system one * feat: reworked logic & improved UI/UX * chore: better wording with dates * fix: checking for index of of current message instead of checking for user messages amount * fix(onboarding): updated GIF and links for final screen * fix: updated tour ending * 2.0.4-alpha.3 * fix: fixed validation of paths for puzzle links on Windows, adjusted texts * improve(puzzle_links): extractLinkFromPuzzle unit tests coverage * fix: tour blinking on restart & typos & unified titles for buttons & removed conditions for set up integrations button * feat: better controls for agentic features * feat: showing deepseek reasoner option only for PRO users, placing it in a correct place of all agentic features * chore: fix component file from merge conflicts * feat: better wording & hiding agent features if restored thread tool use is not agent * chore: better semantic name for reasoning switch component * fix: useGetUser to get user instead of outdated useLogin * 2.0.5-alpha.1 * ui(knowledge): move like button to bottom of last assistant message. * fix: cut off for mock data in agent rollback * 2.0.5-alpha.2 * wip(knowledge): make tool button look nicer. * wip(knowledge): add spacing around tool content. * wip(knowledge): add msw handler for updating usage. --------- Co-authored-by: alashchev17 Co-authored-by: Andrii Lashchov <84778697+alashchev17@users.noreply.github.com> --- src/__fixtures__/chat.ts | 570 ++++++++++++++++++ src/__fixtures__/knowledge.ts | 260 ++++++++ src/__fixtures__/msw.ts | 99 +++ src/app/store.ts | 10 +- src/components/Chat/Chat.stories.tsx | 36 +- src/components/ChatContent/AssistantInput.tsx | 4 + src/components/ChatContent/ChatContent.tsx | 3 + .../ChatContent/LikeButton.module.css | 20 + src/components/ChatContent/LikeButton.tsx | 80 +++ src/components/ChatContent/ToolsContent.tsx | 171 +++++- src/components/ChatForm/ChatForm.tsx | 2 - .../useCommandCompletionAndPreviewFiles.ts | 9 +- src/components/ChatForm/utils.ts | 1 - src/components/Sidebar/Sidebar.tsx | 2 + src/components/Toolbar/Dropdown.tsx | 23 +- src/components/Toolbar/Toolbar.tsx | 3 + src/events/index.ts | 6 + src/features/App.tsx | 2 + src/features/Chat/Thread/types.ts | 1 + src/features/Chat/currentProject.ts | 31 + src/features/Config/FeatureMenu.tsx | 127 ++++ src/features/Config/configSlice.ts | 35 +- src/features/History/historySlice.ts | 9 +- .../Knowledge/KnowledgeForms.module.css | 12 + src/features/Knowledge/KnowledgeForms.tsx | 177 ++++++ .../Knowledge/KnowledgeList.stories.tsx | 52 ++ src/features/Knowledge/KnowledgeList.tsx | 155 +++++ src/features/Knowledge/VecDbStatus.module.css | 17 + src/features/Knowledge/VecdbStatus.tsx | 94 +++ src/features/Knowledge/index.ts | 1 + src/features/Knowledge/knowledgeSlice.ts | 51 ++ src/features/Knowledge/useKnowledgeSearch.ts | 60 ++ src/features/Pages/pagesSlice.ts | 5 + src/hooks/useEventBusForApp.ts | 9 + src/main.tsx | 2 +- src/services/refact/commands.ts | 2 +- src/services/refact/consts.ts | 7 + src/services/refact/index.ts | 1 + src/services/refact/knowledge.ts | 384 ++++++++++++ src/services/refact/types.ts | 87 +++ 40 files changed, 2592 insertions(+), 28 deletions(-) create mode 100644 src/__fixtures__/knowledge.ts create mode 100644 src/components/ChatContent/LikeButton.module.css create mode 100644 src/components/ChatContent/LikeButton.tsx create mode 100644 src/features/Chat/currentProject.ts create mode 100644 src/features/Config/FeatureMenu.tsx create mode 100644 src/features/Knowledge/KnowledgeForms.module.css create mode 100644 src/features/Knowledge/KnowledgeForms.tsx create mode 100644 src/features/Knowledge/KnowledgeList.stories.tsx create mode 100644 src/features/Knowledge/KnowledgeList.tsx create mode 100644 src/features/Knowledge/VecDbStatus.module.css create mode 100644 src/features/Knowledge/VecdbStatus.tsx create mode 100644 src/features/Knowledge/index.ts create mode 100644 src/features/Knowledge/knowledgeSlice.ts create mode 100644 src/features/Knowledge/useKnowledgeSearch.ts create mode 100644 src/services/refact/knowledge.ts diff --git a/src/__fixtures__/chat.ts b/src/__fixtures__/chat.ts index 36cd5f151..20fdc32f2 100644 --- a/src/__fixtures__/chat.ts +++ b/src/__fixtures__/chat.ts @@ -911,3 +911,573 @@ 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, + createdAt: "2025-01-27T13:19:11.132Z", + updatedAt: "2025-01-27T13:19:11.132Z", +}; diff --git a/src/__fixtures__/knowledge.ts b/src/__fixtures__/knowledge.ts new file mode 100644 index 000000000..834ea7002 --- /dev/null +++ b/src/__fixtures__/knowledge.ts @@ -0,0 +1,260 @@ +import { MemdbSubEvent, MemoRecord, VecDbStatus } from "../events"; + +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..a1feafd96 100644 --- a/src/__fixtures__/msw.ts +++ b/src/__fixtures__/msw.ts @@ -5,9 +5,15 @@ 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"; export const goodPing: HttpHandler = http.get( "http://127.0.0.1:8001/v1/ping", @@ -103,6 +109,17 @@ 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: "", + links: [], + }; + return HttpResponse.json(res); + }, +); + export const goodTools: HttpHandler = http.get( `http://127.0.0.1:8001${AT_TOOLS_AVAILABLE_URL}`, () => { @@ -110,6 +127,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", () => { diff --git a/src/app/store.ts b/src/app/store.ts index 8a3a45677..656ffc04c 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -22,6 +22,7 @@ import { integrationsApi, dockerApi, telemetryApi, + knowledgeApi, } from "../services/refact"; import { smallCloudApi } from "../services/smallcloud"; import { reducer as fimReducer } from "../features/FIM/reducer"; @@ -47,6 +48,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 +86,7 @@ const rootReducer = combineSlices( [agentUsageSlice.reducerPath]: persistedAgentUsageReducer, config: configReducer, active_file: activeFileReducer, + current_project: currentProjectInfoReducer, selected_snippet: selectedSnippetReducer, chat: chatReducer, [statisticsApi.reducerPath]: statisticsApi.reducer, @@ -97,6 +101,7 @@ const rootReducer = combineSlices( [linksApi.reducerPath]: linksApi.reducer, [checkpointsApi.reducerPath]: checkpointsApi.reducer, [telemetryApi.reducerPath]: telemetryApi.reducer, + [knowledgeApi.reducerPath]: knowledgeApi.reducer, }, historySlice, errorSlice, @@ -108,6 +113,8 @@ const rootReducer = combineSlices( attachedImagesSlice, userSurveySlice, integrationsSlice, + agentUsageSlice, + knowledgeSlice, checkpointsSlice, patchesAndDiffsTrackerSlice, ); @@ -166,7 +173,7 @@ export function setUpStore(preloadedState?: Partial) { return ( middleware - .concat( + .prepend( pingApi.middleware, statisticsApi.middleware, capsApi.middleware, @@ -181,6 +188,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/Chat/Chat.stories.tsx b/src/components/Chat/Chat.stories.tsx index 825761b5b..60fa618a9 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"; @@ -115,3 +120,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/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.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..5bece56e4 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,9 @@ 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"; type ResultProps = { children: string; @@ -204,24 +217,36 @@ 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 (result && isMultiModalToolResult(result)) { const restInTail = takeWhile(tail, (toolCall) => { const nextResult = toolResults.find( @@ -244,7 +269,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 +289,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 +482,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/ChatForm.tsx b/src/components/ChatForm/ChatForm.tsx index 977e721e9..7c4659619 100644 --- a/src/components/ChatForm/ChatForm.tsx +++ b/src/components/ChatForm/ChatForm.tsx @@ -157,7 +157,6 @@ export const ChatForm: React.FC = ({ const valueIncludingChecks = addCheckboxValuesToInput( trimmedValue, checkboxes, - config.features?.vecdb ?? false, ); setFileInteracted(false); setLineSelectionInteracted(false); @@ -171,7 +170,6 @@ export const ChatForm: React.FC = ({ disableSend, dispatch, checkboxes, - config.features?.vecdb, setFileInteracted, setLineSelectionInteracted, onSubmit, 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/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/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/Toolbar/Dropdown.tsx b/src/components/Toolbar/Dropdown.tsx index 9bb02e1e9..bae9d1c99 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,7 +159,19 @@ export const Dropdown: React.FC = ({ handleNavigation("integrations")}> - Set up Agent Integrations + Setup Agent Integrations + + + {knowledgeEnabled && ( + handleNavigation("knowledge list")} + > + Manage Knowledge + + )} + + handleNavigation("hot keys")}> + IDE Hotkeys Settings handleNavigation("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/events/index.ts b/src/events/index.ts index f9956c5d6..dfde87329 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -18,6 +18,7 @@ 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"; @@ -29,6 +30,10 @@ 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"); @@ -44,6 +49,7 @@ export type InitialState = { history: HistoryState; error: ErrorSliceState; pages: PageSliceState; + current_project: CurrentProjectInfo; }; export { 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/types.ts b/src/features/Chat/Thread/types.ts index 588c8697e..63f868a36 100644 --- a/src/features/Chat/Thread/types.ts +++ b/src/features/Chat/Thread/types.ts @@ -20,6 +20,7 @@ export type ChatThread = { isTitleGenerated?: boolean; integration?: IntegrationMeta | null; mode?: LspChatMode; + project_name?: string; last_user_message_id?: string; }; 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} +