diff --git a/src/__fixtures__/chat_config_thread.ts b/src/__fixtures__/chat_config_thread.ts new file mode 100644 index 00000000..a5dbb748 --- /dev/null +++ b/src/__fixtures__/chat_config_thread.ts @@ -0,0 +1,471 @@ +import type { Chat } from "../features/Chat/Thread"; + +export const CHAT_CONFIG_THREAD: Chat = { + streaming: false, + thread: { + id: "941fb8f4-409c-4430-a3b2-6450fafdb9f4", + messages: [ + { + role: "user", + content: + "🔧 The postgres tool should be visible now. To test the tool, list the tables available, briefly desctibe the tables and express\nsatisfaction and relief if it works, and change nothing. If it doesn't work or the tool isn't available, go through the usual plan in the system prompt.\nThe current config file is .\n", + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "call_IkNfXpwhNVR6D1Sr2CDA5Cfi", + function: { + arguments: + '{"query":"\\nSELECT table_name\\nFROM information_schema.tables\\nWHERE table_schema=\'public\';"}', + name: "postgres", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "call_IkNfXpwhNVR6D1Sr2CDA5Cfi", + content: + "🧩 for configuration go to SETTINGS:postgres, psql failed:\nNo such file or directory (os error 2)", + }, + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "call_kw6TJChemYjXEGL9mLL3T0mN", + function: { + arguments: '{"path":"/Users/marc/Projects/refact-lsp"}', + name: "tree", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "call_kw6TJChemYjXEGL9mLL3T0mN", + content: + "/\n Users/\n marc/\n Projects/\n refact-lsp/\n .dockerignore\n .gitattributes\n .gitignore\n CODE_OF_CONDUCT.md\n CONTRIBUTING.md\n Cargo.lock\n Cargo.toml\n Cross.toml\n INTEGRATIONS.md\n LICENSE\n README.md\n build.rs\n tests/\n __init__.py\n lsp_connect.py\n test01_completion_edge_cases.py\n test02_completion_with_rag.py\n test03_at_commands_completion.py\n test04_completion_lsp.py\n test05_is_openai_compatible.py\n test06_tool_not_tool.py\n test07_memories.py\n test08_post_processing.py\n test09_ast_pick_up_changes.py\n test10_locate.py\n test11_patch.py\n test11_patch_partial_edit.py\n test12_tools_authorize_calls.py\n test13_vision.py\n test_diff_handlers.py\n test13_data/\n 200.jpg\n 530.jpg\n test11_data/\n already_applied_rewrite_symbol_01.py\n already_applied_rewrite_symbol_02.py\n toad_orig.py\n toad_partial_edit_01.py\n toad_partial_edit_02.py\n toad_rewrite_symbol_01.py\n toad_rewrite_symbol_02.py\n toad_rewrite_symbol_03.py\n toad_rewrite_symbol_04_orig.rs\n toad_rewrite_symbol_04_patched.rs\n emergency_frog_situation/\n frog.py\n holiday.py\n jump_to_conclusions.py\n set_as_avatar.py\n work_day.py\n src/\n background_tasks.rs\n cached_tokenizers.rs\n call_validation.rs\n caps.rs\n completion_cache.rs\n custom_error.rs\n diffs.rs\n fetch_embedding.rs\n file_filter.rs\n files_correction.rs\n files_in_jsonl.rs\n files_in_workspace.rs\n forward_to_hf_endpoint.rs\n forward_to_openai_endpoint.rs\n fuzzy_search.rs\n git.rs\n global_context.rs\n http.rs\n knowledge.rs\n known_models.rs\n lsp.rs\n main.rs\n nicer_logs.rs\n privacy.rs\n privacy_compiled_in.rs\n restream.rs\n scratchpad_abstract.rs\n subchat.rs\n version.rs\n yaml_configs/\n create_configs.rs\n customization_compiled_in.rs\n customization_loader.rs\n mod.rs\n vecdb/\n mod.rs\n vdb_cache.rs\n vdb_file_splitter.rs\n vdb_highlev.rs\n vdb_lance.rs\n vdb_remote.rs\n vdb_structs.rs\n vdb_thread.rs\n tools/\n mod.rs\n tool_ast_definition.rs\n tool_ast_reference.rs\n tool_cat.rs\n tool_cmdline.rs\n tool_deep_thinking.rs\n tool_knowledge.rs\n tool_locate_search.rs\n tool_patch.rs\n tool_relevant_files.rs\n tool_search.rs\n tool_tree.rs\n tool_web.rs\n tools_description.rs\n tools_execute.rs\n tool_patch_aux/\n ast_lint.rs\n diff_apply.rs\n diff_structs.rs\n fs_utils.rs\n mod.rs\n no_model_edit.rs\n postprocessing_utils.rs\n tickets_parsing.rs\n model_based_edit/\n blocks_of_code_parser.rs\n mod.rs\n model_execution.rs\n partial_edit.rs\n whole_file_parser.rs\n telemetry/\n basic_comp_counters.rs\n basic_network.rs\n basic_robot_human.rs\n basic_transmit.rs\n mod.rs\n snippets_collection.rs\n snippets_transmit.rs\n telemetry_structs.rs\n utils.rs\n scratchpads/\n chat_generic.rs\n chat_llama2.rs\n chat_passthrough.rs\n chat_utils_deltadelta.rs\n chat_utils_limit_history.rs\n chat_utils_prompts.rs\n code_completion_fim.rs\n code_completion_replace.rs\n comments_parser.rs\n mod.rs\n multimodality.rs\n passthrough_convert_messages.rs\n scratchpad_utils.rs\n postprocessing/\n mod.rs\n pp_command_output.rs\n pp_context_files.rs\n pp_plain_text.rs\n pp_utils.rs\n integrations/\n config_chat.rs\n integr_abstract.rs\n integr_chrome.rs\n integr_github.rs\n integr_gitlab.rs\n integr_pdb.rs\n integr_postgres.rs\n mod.rs\n process_io_utils.rs\n running_integrations.rs\n sessions.rs\n setting_up_integrations.rs\n yaml_schema.rs\n docker/\n docker_container_manager.rs\n docker_ssh_tunnel_utils.rs\n integr_docker.rs\n mod.rs\n http/\n routers.rs\n utils.rs\n routers/\n info.rs\n v1.rs\n v1/\n ast.rs\n at_commands.rs\n at_tools.rs\n caps.rs\n chat.rs\n code_completion.rs\n code_lens.rs\n customization.rs\n dashboard.rs\n docker.rs\n git.rs\n graceful_shutdown.rs\n gui_help_handlers.rs\n handlers_memdb.rs\n links.rs\n lsp_like_handlers.rs\n patch.rs\n snippet_accepted.rs\n status.rs\n subchat.rs\n sync_files.rs\n system_prompt.rs\n telemetry_network.rs\n v1_integrations.rs\n vecdb.rs\n dashboard/\n dashboard.rs\n mod.rs\n structs.rs\n utils.rs\n at_commands/\n at_ast_definition.rs\n at_ast_reference.rs\n at_commands.rs\n at_file.rs\n at_search.rs\n at_tree.rs\n at_web.rs\n execute_at.rs\n mod.rs\n ast/\n ast_db.rs\n ast_indexer_thread.rs\n ast_parse_anything.rs\n ast_structs.rs\n chunk_utils.rs\n dummy_tokenizer.json\n file_splitter.rs\n linters.rs\n mod.rs\n parse_common.rs\n parse_python.rs\n treesitter/\n ast_instance_structs.rs\n file_ast_markup.rs\n language_id.rs\n mod.rs\n parsers.rs\n skeletonizer.rs\n structs.rs\n parsers/\n cpp.rs\n java.rs\n js.rs\n python.rs\n rust.rs\n tests.rs\n ts.rs\n utils.rs\n tests/\n cpp.rs\n java.rs\n js.rs\n python.rs\n rust.rs\n ts.rs\n cases/\n ts/\n main.ts\n main.ts.json\n person.ts\n person.ts.decl_json\n person.ts.skeleton\n rust/\n main.rs\n main.rs.json\n point.rs\n point.rs.decl_json\n point.rs.skeleton\n python/\n calculator.py\n calculator.py.decl_json\n calculator.py.skeleton\n main.py\n main.py.json\n js/\n car.js\n car.js.decl_json\n car.js.skeleton\n main.js\n main.js.json\n java/\n main.java\n main.java.json\n person.java\n person.java.decl_json\n person.java.skeleton\n cpp/\n circle.cpp\n circle.cpp.decl_json\n circle.cpp.skeleton\n main.cpp\n main.cpp.json\n alt_testsuite/\n cpp_goat_library.correct\n cpp_goat_library.h\n cpp_goat_main.correct\n cpp_goat_main.cpp\n jump_to_conclusions_annotated.py\n py_goat_library.correct\n py_goat_library.py\n py_goat_library_annotated.py\n py_goat_main.py\n py_goat_main_annotated.py\n py_torture1_attr.py\n py_torture1_attr_annotated.py\n py_torture2_resolving.py\n py_torture2_resolving_annotated.py\n python_binding_and_cmdline/\n setup.py\n refact/\n __init__.py\n chat_client.py\n cli_app_switcher.py\n cli_export.py\n cli_inspect.py\n cli_main.py\n cli_markdown.py\n cli_printing.py\n cli_settings.py\n cli_statusbar.py\n cli_streaming.py\n lsp_runner.py\n traj_compressor.py\n examples/\n ast_definition.sh\n ast_references.sh\n chat_with_at_command.py\n http_caps.sh\n http_chat.sh\n http_chat_passthrough.sh\n http_completion.sh\n http_rag_status.sh\n http_subchat.sh\n http_vecdb_search.sh\n lsp_runner.py\n note3.py\n rag_skeletonize_video.py\n docker/\n lsp-debug.Dockerfile\n chrome/\n mac_arm-130.0.6723.69/\n chrome-mac-arm64/\n Google Chrome for Testing.app/\n Contents/\n Resources/\n com.google.chrome.for.testing.manifest/\n Contents/\n Resources/\n com.google.chrome.for.testing.manifest\n en.lproj/\n Localizable.strings\n Frameworks/\n Google Chrome for Testing Framework.framework/\n Versions/\n 130.0.6723.69/\n Libraries/\n WidevineCdm/\n _platform_specific/\n mac_arm64/\n libwidevinecdm.dylib\n bring_your_own_key/\n hf.yaml\n mixed.yaml\n openai.yaml\n openrouter.yaml", + }, + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "call_4we2wH5H50A2m6CIJqKbzYH8", + function: { + arguments: '{"paths": "Cargo.toml", "symbols": ""}', + name: "cat", + }, + type: "function", + index: 0, + }, + { + id: "call_vmIGl31ytfpLWPkc138HJnxz", + function: { + arguments: '{"paths": "README.md", "symbols": ""}', + name: "cat", + }, + type: "function", + index: 1, + }, + { + id: "call_QD1oyHwPOvvFdYUfV3ijiKzB", + function: { + arguments: + '{"paths": "tests/emergency_frog_situation/frog.py", "symbols": ""}', + name: "cat", + }, + type: "function", + index: 2, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "call_QD1oyHwPOvvFdYUfV3ijiKzB", + content: + "Paths found:\n/Users/marc/Projects/refact-lsp/tests/emergency_frog_situation/frog.py\n", + }, + }, + { + role: "tool", + content: { + tool_call_id: "call_vmIGl31ytfpLWPkc138HJnxz", + content: + 'Path problems:\n\nThe path "README.md" does not exist. There are no similar names either.\n', + }, + }, + { + role: "tool", + content: { + tool_call_id: "call_4we2wH5H50A2m6CIJqKbzYH8", + content: + 'Path problems:\n\nThe path "Cargo.toml" does not exist. There are no similar names either.\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: "", + tool_calls: [ + { + id: "call_sNnvyOLlmulfHIikJzKhmz55", + function: { + arguments: '{"paths": "src/Cargo.toml", "symbols": ""}', + name: "cat", + }, + type: "function", + index: 0, + }, + { + id: "call_n9FACVDl6a4tllgyWaLCl8s9", + function: { + arguments: '{"paths": "src/main.rs", "symbols": ""}', + name: "cat", + }, + type: "function", + index: 1, + }, + { + id: "call_1DUkSfLq6Io8xowqVjJEuybM", + function: { + arguments: '{"paths": "src/lib.rs", "symbols": ""}', + name: "cat", + }, + type: "function", + index: 2, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "call_1DUkSfLq6Io8xowqVjJEuybM", + content: + 'Path problems:\n\nThe path "src/lib.rs" does not exist. There are no similar names either.\n', + }, + }, + { + role: "tool", + content: { + tool_call_id: "call_n9FACVDl6a4tllgyWaLCl8s9", + content: + 'Path problems:\n\nThe path "src/main.rs" does not exist. There are no similar names either.\n', + }, + }, + { + role: "tool", + content: { + tool_call_id: "call_sNnvyOLlmulfHIikJzKhmz55", + content: + 'Path problems:\n\nThe path "src/Cargo.toml" does not exist. There are no similar names either.\n', + }, + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "call_h8biZ8JNUjeWGbL9CCatbPcf", + function: { + arguments: '{"paths":"src/background_tasks.rs","symbols":""}', + name: "cat", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "call_h8biZ8JNUjeWGbL9CCatbPcf", + content: + 'Path problems:\n\nThe path "src/background_tasks.rs" does not exist. There are no similar names either.\n', + }, + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "call_coI5PDRF2O2Mjcfb8bdZ4Heo", + function: { + arguments: + '{"paths":"src/tools/integr_postgres.rs","symbols":""}', + name: "cat", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "call_coI5PDRF2O2Mjcfb8bdZ4Heo", + content: + 'Path problems:\n\nThe path "src/tools/integr_postgres.rs" does not exist. There are no similar names either.\n', + }, + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "call_RXDRwr3yCJEplHud4xRoxWlC", + function: { + arguments: + '{"paths":"src/integrations/integr_postgres.rs","symbols":""}', + name: "cat", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "call_RXDRwr3yCJEplHud4xRoxWlC", + content: + 'Path problems:\n\nThe path "src/integrations/integr_postgres.rs" does not exist. There are no similar names either.\n', + }, + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "call_OvYAforNKAFz0d7mv96AnTVR", + function: { + arguments: + '{"paths":"src/integrations/integr_abstract.rs","symbols":""}', + name: "cat", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "call_OvYAforNKAFz0d7mv96AnTVR", + content: + 'Path problems:\n\nThe path "src/integrations/integr_abstract.rs" does not exist. There are no similar names either.\n', + }, + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "call_3wuMILBYl0z7pBMUoJOwohQf", + function: { + arguments: + '{"paths":"src/integrations/integr_github.rs","symbols":""}', + name: "cat", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "call_3wuMILBYl0z7pBMUoJOwohQf", + content: + 'Path problems:\n\nThe path "src/integrations/integr_github.rs" does not exist. There are no similar names either.\n', + }, + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "call_AzBRj8RiL1wc8eejuBVdaS6t", + function: { + arguments: + '{"paths":"src/integrations/integr_gitlab.rs","symbols":""}', + name: "cat", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "call_AzBRj8RiL1wc8eejuBVdaS6t", + content: + 'Path problems:\n\nThe path "src/integrations/integr_gitlab.rs" does not exist. There are no similar names either.\n', + }, + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "call_bJ2MrMSJHk4IF6Gp5DNLhJZP", + function: { + arguments: + '{"paths":"src/integrations/integr_chrome.rs","symbols":""}', + name: "cat", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "call_bJ2MrMSJHk4IF6Gp5DNLhJZP", + content: + 'Path problems:\n\nThe path "src/integrations/integr_chrome.rs" does not exist. There are no similar names either.\n', + }, + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "call_kZFTzONZdyo11FQcSudIo3vK", + function: { + arguments: + '{"paths":"src/integrations/integr_pdb.rs","symbols":""}', + name: "cat", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "call_kZFTzONZdyo11FQcSudIo3vK", + content: + 'Path problems:\n\nThe path "src/integrations/integr_pdb.rs" does not exist. There are no similar names either.\n', + }, + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "call_MDynldaxbGEuCKSuQg0Vgk5z", + function: { + arguments: + '{"paths":"src/integrations/integr_docker.rs","symbols":""}', + name: "cat", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "call_MDynldaxbGEuCKSuQg0Vgk5z", + content: + 'Path problems:\n\nThe path "src/integrations/integr_docker.rs" does not exist. There are no similar names either.\n', + }, + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "call_9fCGZwstx7G1MgHs6JD5JWTn", + function: { + arguments: + '{"paths":"src/integrations/integr_abstract.rs","symbols":""}', + name: "cat", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "call_9fCGZwstx7G1MgHs6JD5JWTn", + content: + 'Path problems:\n\nThe path "src/integrations/integr_abstract.rs" does not exist. There are no similar names either.\n', + }, + }, + { + role: "assistant", + content: "", + tool_calls: [ + { + id: "call_etmMcI1UwBSaWwZHzxsuL8xu", + function: { + arguments: + '{"paths":"src/integrations/integr_postgres.rs","symbols":""}', + name: "cat", + }, + type: "function", + index: 0, + }, + ], + }, + { + role: "tool", + content: { + tool_call_id: "call_etmMcI1UwBSaWwZHzxsuL8xu", + content: + 'Path problems:\n\nThe path "src/integrations/integr_postgres.rs" does not exist. There are no similar names either.\n', + }, + }, + ], + title: + "🔧 The postgres tool should be visible now. To test the tool, list the tables available, briefly desctibe the tables and express\nsatisfaction and relief if it works, and change nothing. If it doesn't work or the tool isn't available, go through the usual plan in the system prompt.\nThe current config file is .\n", + model: "", + tool_use: "agent", + integration: { + name: "postgres", + path: "", + project: "", + }, + read: true, + createdAt: "2024-12-02T14:42:18.902Z", + updatedAt: "2024-12-02T14:42:18.902Z", + }, + error: null, + prevent_send: true, + waiting_for_response: false, + cache: {}, + system_prompt: {}, + tool_use: "agent", + send_immediately: false, +}; diff --git a/src/__fixtures__/chat_links_response.ts b/src/__fixtures__/chat_links_response.ts new file mode 100644 index 00000000..f2fecc5f --- /dev/null +++ b/src/__fixtures__/chat_links_response.ts @@ -0,0 +1,15 @@ +import { LinksForChatResponse } from "../services/refact/links"; + +export const STUB_LINKS_FOR_CHAT_RESPONSE: LinksForChatResponse = { + links: [ + { + text: "Save and return", + action: "patch-all", + goto: "SETTINGS:/path/to/config/file.yaml", + }, + { text: "Can you fix it?", action: "follow-up" }, + { text: 'git commit -m "message"', action: "commit" }, + { text: "Save and return", goto: "SETTINGS:postgres" }, + { text: "Investigate Project", action: "summarize-project" }, + ], +}; diff --git a/src/__fixtures__/index.ts b/src/__fixtures__/index.ts index 4bec7086..4110e97d 100644 --- a/src/__fixtures__/index.ts +++ b/src/__fixtures__/index.ts @@ -5,3 +5,5 @@ export * from "./context_files"; export * from "./prompts"; export * from "./integrations"; export * from "./survey_questions"; +export * from "./chat_links_response"; +export * from "./chat_config_thread"; diff --git a/src/app/store.ts b/src/app/store.ts index 5c91215c..3807bbc6 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -43,6 +43,7 @@ import { informationSlice } from "../features/Errors/informationSlice"; import { confirmationSlice } from "../features/ToolConfirmation/confirmationSlice"; import { attachedImagesSlice } from "../features/AttachedImages"; import { userSurveySlice } from "../features/UserSurvey/userSurveySlice"; +import { linksApi } from "../services/refact/links"; import { integrationsSlice } from "../features/Integrations"; const tipOfTheDayPersistConfig = { @@ -77,6 +78,7 @@ const rootReducer = combineSlices( [smallCloudApi.reducerPath]: smallCloudApi.reducer, [pathApi.reducerPath]: pathApi.reducer, [pingApi.reducerPath]: pingApi.reducer, + [linksApi.reducerPath]: linksApi.reducer, }, historySlice, errorSlice, @@ -149,6 +151,7 @@ export function setUpStore(preloadedState?: Partial) { diffApi.middleware, smallCloudApi.middleware, pathApi.middleware, + linksApi.middleware, integrationsApi.middleware, dockerApi.middleware, ) diff --git a/src/components/ChatContent/ChatContent.stories.tsx b/src/components/ChatContent/ChatContent.stories.tsx index ee4b6353..785ed156 100644 --- a/src/components/ChatContent/ChatContent.stories.tsx +++ b/src/components/ChatContent/ChatContent.stories.tsx @@ -6,7 +6,7 @@ import { setUpStore } from "../../app/store"; import { Theme } from "../Theme"; import { AbortControllerProvider } from "../../contexts/AbortControllers"; import { MarkdownMessage } from "../../__fixtures__/markdown"; -import { ChatMessages } from "../../events"; +import { ChatMessages, ChatThread } from "../../events"; import { CHAT_FUNCTIONS_MESSAGES, CHAT_WITH_DIFF_ACTIONS, @@ -14,11 +14,21 @@ import { FROG_CHAT, LARGE_DIFF, CHAT_WITH_MULTI_MODAL, + CHAT_CONFIG_THREAD, + STUB_LINKS_FOR_CHAT_RESPONSE, } from "../../__fixtures__"; +import { http, HttpResponse } from "msw"; +import { CHAT_LINKS_URL } from "../../services/refact/consts"; const MockedStore: React.FC<{ - messages: ChatMessages; -}> = ({ messages }) => { + messages?: ChatMessages; + thread?: ChatThread; +}> = ({ messages, thread }) => { + const threadData = thread ?? { + id: "test", + model: "test", + messages: messages ?? [], + }; const store = setUpStore({ chat: { streaming: false, @@ -29,11 +39,7 @@ const MockedStore: React.FC<{ error: null, cache: {}, system_prompt: {}, - thread: { - id: "test", - model: "test", - messages, - }, + thread: threadData, }, }); @@ -119,3 +125,18 @@ export const MultiModal: Story = { messages: CHAT_WITH_MULTI_MODAL.messages, }, }; + +export const IntegrationChat: Story = { + args: { + thread: CHAT_CONFIG_THREAD.thread, + }, + parameters: { + msw: { + handlers: [ + http.post(CHAT_LINKS_URL, () => { + return HttpResponse.json(STUB_LINKS_FOR_CHAT_RESPONSE); + }), + ], + }, + }, +}; diff --git a/src/components/ChatContent/ChatContent.tsx b/src/components/ChatContent/ChatContent.tsx index d15f052a..6d783cd6 100644 --- a/src/components/ChatContent/ChatContent.tsx +++ b/src/components/ChatContent/ChatContent.tsx @@ -30,6 +30,7 @@ import { GroupedDiffs } from "./DiffContent"; import { ScrollToBottomButton } from "./ScrollToBottomButton"; import { currentTipOfTheDay } from "../../features/TipOfTheDay"; import { popBackTo } from "../../features/Pages/pagesSlice"; +import { ChatLinks } from "../ChatLinks"; const TipOfTheDay: React.FC = () => { const tip = useAppSelector(currentTipOfTheDay); @@ -150,22 +151,23 @@ export const ChatContent: React.FC = ({ }; const handleReturnToConfigurationClick = useCallback(() => { - // eslint-disable-next-line no-console - console.log(`[DEBUG]: going back to configuration page`); + // console.log(`[DEBUG]: going back to configuration page`); // TBD: should it be allowed to run in the background? onStopStreaming(); dispatch( popBackTo({ name: "integrations page", - projectPath: thread.integration?.path, + projectPath: thread.integration?.project, integrationName: thread.integration?.name, + integrationPath: thread.integration?.path, }), ); }, [ + onStopStreaming, dispatch, + thread.integration?.project, thread.integration?.name, thread.integration?.path, - onStopStreaming, ]); const handleSaveAndReturn = useCallback(async () => { @@ -187,6 +189,9 @@ export const ChatContent: React.FC = ({ {messages.length === 0 && } {renderMessages(messages, onRetryWrapper)} + + + diff --git a/src/components/ChatLinks/ChatLinks.module.css b/src/components/ChatLinks/ChatLinks.module.css new file mode 100644 index 00000000..e69de29b diff --git a/src/components/ChatLinks/ChatLinks.stories.tsx b/src/components/ChatLinks/ChatLinks.stories.tsx new file mode 100644 index 00000000..8bebc203 --- /dev/null +++ b/src/components/ChatLinks/ChatLinks.stories.tsx @@ -0,0 +1,53 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import { ChatLinks } from "./ChatLinks"; +import { setUpStore } from "../../app/store"; +import { Provider } from "react-redux"; +import { Theme } from "../Theme"; +import { Container } from "@radix-ui/themes"; +import { http, HttpResponse, type HttpHandler } from "msw"; +import { CHAT_LINKS_URL } from "../../services/refact/consts"; +import { + STUB_LINKS_FOR_CHAT_RESPONSE, + CHAT_CONFIG_THREAD, +} from "../../__fixtures__"; + +const Template = () => { + const store = setUpStore({ + chat: CHAT_CONFIG_THREAD, + }); + return ( + + + + + + + + ); +}; + +const meta = { + title: "Components/ChatLinks", + component: Template, + argTypes: { + //... + }, + parameters: { + msw: { + handlers: [ + http.post(CHAT_LINKS_URL, () => { + return HttpResponse.json(STUB_LINKS_FOR_CHAT_RESPONSE); + }), + ], + }, + }, +} satisfies Meta< + typeof Template & { parameters: { msw: { handlers: HttpHandler[] } } } +>; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/components/ChatLinks/ChatLinks.tsx b/src/components/ChatLinks/ChatLinks.tsx new file mode 100644 index 00000000..615a93b4 --- /dev/null +++ b/src/components/ChatLinks/ChatLinks.tsx @@ -0,0 +1,213 @@ +import React, { useEffect } from "react"; +import { Flex, Button, Heading, Container } from "@radix-ui/themes"; +import { linksApi, type ChatLink } from "../../services/refact/links"; +import { diffApi, isUserMessage } from "../../services/refact"; +import { + useAppDispatch, + useAppSelector, + useEventsBusForIDE, + useGetCapsQuery, + useSendChatRequest, +} from "../../hooks"; +import { + chatModeToLspMode, + selectChatId, + selectIntegration, + selectIsStreaming, + selectIsWaiting, + selectMessages, + selectModel, + selectThreadToolUse, +} from "../../features/Chat"; +import { popBackTo } from "../../features/Pages/pagesSlice"; + +function maybeConcatActionAndGoToStrings(link: ChatLink): string | undefined { + const hasAction = "action" in link; + const hasGoTo = "goto" in link; + if (!hasAction && !hasGoTo) return ""; + if (hasAction && hasGoTo) return `action: ${link.action}\ngoto: ${link.goto}`; + if (hasAction) return `action: ${link.action}`; + return `goto: ${link.goto}`; +} + +const isAbsolutePath = (path: string) => { + const absolutePathRegex = /^(?:[a-zA-Z]:\\|\/|\\\\|\/\/).*/; + return absolutePathRegex.test(path); +}; + +export const ChatLinks: React.FC = () => { + const dispatch = useAppDispatch(); + const { queryPathThenOpenFile } = useEventsBusForIDE(); + const { submit } = useSendChatRequest(); + + const [applyPatches, _applyPatchesResult] = + diffApi.useApplyAllPatchesInMessagesMutation(); + + const isStreaming = useAppSelector(selectIsStreaming); + const isWaiting = useAppSelector(selectIsWaiting); + const messages = useAppSelector(selectMessages); + const chatId = useAppSelector(selectChatId); + const maybeIntegration = useAppSelector(selectIntegration); + const chatMode = useAppSelector(selectThreadToolUse); + + // TODO: add the model + const caps = useGetCapsQuery(); + + const model = + useAppSelector(selectModel) || caps.data?.code_chat_default_model; + + const unCalledTools = React.useMemo(() => { + if (messages.length === 0) return false; + const last = messages[messages.length - 1]; + //TODO: handle multiple tool calls in last assistant message + if (last.role !== "assistant") return false; + const maybeTools = last.tool_calls; + if (maybeTools && maybeTools.length > 0) return true; + return false; + }, [messages]); + + const handleGoTo = (goto?: string) => { + if (!goto) return; + // TODO: duplicated in smart links. + const [action, payload] = goto.split(":"); + + switch (action.toLowerCase()) { + case "editor": { + void queryPathThenOpenFile({ file_name: payload }); + return; + } + case "settings": { + const isFile = isAbsolutePath(payload); + dispatch( + popBackTo({ + name: "integrations page", + // projectPath: isFile ? payload : "", + integrationName: !isFile ? payload : "", + integrationPath: isFile ? payload : "", + }), + ); + // TODO: open in the integrations + return; + } + default: { + // eslint-disable-next-line no-console + console.log(`[DEBUG]: unexpected action, doing nothing`); + return; + } + } + }; + const handleLinkAction = (link: ChatLink) => { + if (!("action" in link)) return; + if (link.action === "goto" && "goto" in link) { + handleGoTo(link.goto); + return; + } + if (link.action === "patch-all") { + void applyPatches(messages); + return; + } + + if (link.action === "follow-up") { + submit(link.text); + return; + } + + if (link.action === "summarize-project") { + submit(link.text, "PROJECTSUMMARY"); + return; + } + + // if (link.action === "commit") { + // ??? + // return; + // } + + // eslint-disable-next-line no-console + console.warn(`unknown action: ${JSON.stringify(link)}`); + }; + const handleClick = (link: ChatLink) => { + if (!("action" in link) && "goto" in link) { + handleGoTo(link.goto); + } else { + handleLinkAction(link); + } + }; + + const [linksRequest, linksResult] = linksApi.useGetLinksForChatMutation(); + + useEffect(() => { + const isEmpty = messages.length === 0; + const lastMessageIsUserMessage = + !isEmpty && isUserMessage(messages[messages.length - 1]); + if ( + !isStreaming && + !isWaiting && + !unCalledTools && + !lastMessageIsUserMessage && + model + ) { + void linksRequest({ + chat_id: chatId, + messages: messages, + model, + mode: maybeIntegration ? "CONFIGURE" : chatModeToLspMode(chatMode), + current_config_file: maybeIntegration?.path, + }); + } + }, [ + chatId, + chatMode, + isStreaming, + isWaiting, + linksRequest, + maybeIntegration, + maybeIntegration?.path, + messages, + model, + unCalledTools, + ]); + + // TODO: waiting, errors, maybe add a title + + if (!linksResult.data || isStreaming || isWaiting || unCalledTools) { + return null; + } + + return ( + + + Available Actions:{" "} + + + + {linksResult.data.links.map((link, index) => { + const key = `chat-link-${index}`; + return ; + })} + + + ); +}; + +const ChatLinkButton: React.FC<{ + link: ChatLink; + onClick: (link: ChatLink) => void; +}> = ({ link, onClick }) => { + const title = maybeConcatActionAndGoToStrings(link); + const handleClick = React.useCallback(() => onClick(link), [link, onClick]); + + return ( + + ); +}; diff --git a/src/components/ChatLinks/index.ts b/src/components/ChatLinks/index.ts new file mode 100644 index 00000000..4a232182 --- /dev/null +++ b/src/components/ChatLinks/index.ts @@ -0,0 +1 @@ +export * from "./ChatLinks"; diff --git a/src/components/IntegrationsView/IntegrationDocker/DockerContainerCard.tsx b/src/components/IntegrationsView/IntegrationDocker/DockerContainerCard.tsx index 84dbd441..25a040e3 100644 --- a/src/components/IntegrationsView/IntegrationDocker/DockerContainerCard.tsx +++ b/src/components/IntegrationsView/IntegrationDocker/DockerContainerCard.tsx @@ -30,6 +30,7 @@ type DockerContainerCardProps = { integrationData: { integrationName: string; integrationPath: string; + integrationProject: string; }; container: DockerContainer; currentContainerAction: DockerActionPayload | null; @@ -186,6 +187,9 @@ export const DockerContainerCard: FC = ({ smartlink={link} integrationName={integrationData.integrationName} integrationPath={integrationData.integrationPath} + integrationProject={ + integrationData.integrationProject + } isDockerSmartlink /> ))} diff --git a/src/components/IntegrationsView/IntegrationDocker/IntegrationDocker.tsx b/src/components/IntegrationsView/IntegrationDocker/IntegrationDocker.tsx index 82c0fdb8..757b40c8 100644 --- a/src/components/IntegrationsView/IntegrationDocker/IntegrationDocker.tsx +++ b/src/components/IntegrationsView/IntegrationDocker/IntegrationDocker.tsx @@ -24,12 +24,14 @@ type IntegrationDockerProps = { dockerData: SchemaDocker; integrationName: string; integrationPath: string; + integrationProject: string; }; export const IntegrationDocker: FC = ({ dockerData, integrationName, integrationPath, + integrationProject, }) => { const dispatch = useAppDispatch(); const { dockerContainers } = useGetDockerContainersByImageQuery( @@ -134,6 +136,7 @@ export const IntegrationDocker: FC = ({ integrationData={{ integrationName, integrationPath, + integrationProject, }} containerSmartlinks={dockerData.smartlinks_for_each_container} /> @@ -145,6 +148,7 @@ export const IntegrationDocker: FC = ({ key={`docker-container-${dockerData.filter_image}`} integrationName={integrationName} integrationPath={integrationPath} + integrationProject={integrationProject} smartlink={smartlink} /> ))} diff --git a/src/components/IntegrationsView/IntegrationForm/IntegrationForm.tsx b/src/components/IntegrationsView/IntegrationForm/IntegrationForm.tsx index 4c2f21c5..67821ded 100644 --- a/src/components/IntegrationsView/IntegrationForm/IntegrationForm.tsx +++ b/src/components/IntegrationsView/IntegrationForm/IntegrationForm.tsx @@ -149,7 +149,8 @@ export const IntegrationForm: FC = ({ values: integration.data.integr_values, field: integration.data.integr_schema.fields[fieldKey], integrationName: integration.data.integr_name, - integrationPath: integration.data.project_path, + integrationPath: integration.data.integr_config_path, + integrationProject: integration.data.project_path, }); } })} @@ -160,7 +161,8 @@ export const IntegrationForm: FC = ({ values: integration.data.integr_values, field: integration.data.integr_schema.fields[fieldKey], integrationName: integration.data.integr_name, - integrationPath: integration.data.project_path, + integrationPath: integration.data.integr_config_path, + integrationProject: integration.data.project_path, isFieldVisible: areExtraFieldsRevealed, }); } @@ -203,7 +205,10 @@ export const IntegrationForm: FC = ({ key={`smartlink-${index}`} smartlink={smartlink} integrationName={integration.data?.integr_name ?? ""} - integrationPath={integration.data?.project_path ?? ""} + integrationProject={integration.data?.project_path ?? ""} + integrationPath={ + integration.data?.integr_config_path ?? "" + } /> ); }, @@ -227,7 +232,8 @@ export const IntegrationForm: FC = ({ )} diff --git a/src/components/IntegrationsView/IntegrationsView.tsx b/src/components/IntegrationsView/IntegrationsView.tsx index e3b654cf..b87b5eb5 100644 --- a/src/components/IntegrationsView/IntegrationsView.tsx +++ b/src/components/IntegrationsView/IntegrationsView.tsx @@ -76,8 +76,8 @@ export const IntegrationsView: FC = ({ return ( integrationsMap.integrations.find( (integration) => - integration.project_path === currentThreadIntegration.projectPath && - integration.integr_name === currentThreadIntegration.integrationName, + integration.integr_config_path === + currentThreadIntegration.integrationPath, ) ?? null ); }, [currentThreadIntegration, integrationsMap]); diff --git a/src/components/IntegrationsView/JSONFormStyles.css b/src/components/IntegrationsView/JSONFormStyles.css new file mode 100644 index 00000000..b7d2e4e6 --- /dev/null +++ b/src/components/IntegrationsView/JSONFormStyles.css @@ -0,0 +1,29 @@ +fieldset { + border: unset; + padding: 0; + margin: 0; +} + +.form-group { + &:not(:last-child) { + margin-bottom: 1.5rem; + } + &.field-array { + padding: 1.25rem; + border: 1px solid var(--mauve-6); + border-radius: 4px; + } + .control-label { + font-weight: 600; + font-size: 15px; + } + .field-description { + display: block; + opacity: 0.85; + } +} + +.btn-group { + justify-content: space-between !important; + margin-bottom: 1.5rem; +} diff --git a/src/components/SmartLink/SmartLink.tsx b/src/components/SmartLink/SmartLink.tsx index 6ccddf90..7750737e 100644 --- a/src/components/SmartLink/SmartLink.tsx +++ b/src/components/SmartLink/SmartLink.tsx @@ -40,13 +40,18 @@ const handleChatAction = ( dispatch: AppDispatch, integrationName: string, integrationPath: string, + integrationProject: string, ) => { const messages = formatMessagesForChat(sl_chat); dispatch(clearInformation()); dispatch( newIntegrationChat({ - integration: { name: integrationName, path: integrationPath }, + integration: { + name: integrationName, + path: integrationPath, + project: integrationProject, + }, messages, }), ); @@ -57,12 +62,14 @@ export const SmartLink: FC<{ smartlink: SmartLinkType; integrationName: string; integrationPath: string; + integrationProject: string; isSmall?: boolean; isDockerSmartlink?: boolean; }> = ({ smartlink, integrationName, integrationPath, + integrationProject, isSmall = false, isDockerSmartlink = false, }) => { @@ -78,7 +85,13 @@ export const SmartLink: FC<{ return; } if (sl_chat) { - handleChatAction(sl_chat, dispatch, integrationName, integrationPath); + handleChatAction( + sl_chat, + dispatch, + integrationName, + integrationPath, + integrationProject, + ); } }, [ sl_goto, @@ -87,6 +100,7 @@ export const SmartLink: FC<{ integrationName, integrationPath, queryPathThenOpenFile, + integrationProject, ]); const title = sl_chat?.reduce((acc, cur) => { diff --git a/src/features/Chat/Thread/actions.ts b/src/features/Chat/Thread/actions.ts index dc0bb85e..c90f8b5d 100644 --- a/src/features/Chat/Thread/actions.ts +++ b/src/features/Chat/Thread/actions.ts @@ -4,6 +4,7 @@ import { type ChatThread, type PayloadWithId, type ToolUse, + IntegrationMeta, } from "./types"; import { isAssistantMessage, @@ -17,7 +18,7 @@ import { type ChatResponse, } from "../../../services/refact/types"; import type { AppDispatch, RootState } from "../../../app/store"; -import type { SystemPrompts } from "../../../services/refact/prompts"; +import { type SystemPrompts } from "../../../services/refact/prompts"; import { formatMessagesForLsp, consumeStream } from "./utils"; import { generateChatTitle, sendChat } from "../../../services/refact/chat"; import { ToolCommand } from "../../../services/refact/tools"; @@ -26,7 +27,7 @@ import { scanFoDuplicatesWith, takeFromEndWhile } from "../../../utils"; export const newChatAction = createAction("chatThread/new"); export const newIntegrationChat = createAction<{ - integration: { name: string; path: string }; + integration: IntegrationMeta; messages: ChatMessages; }>("chatThread/newIntegrationChat"); @@ -199,15 +200,22 @@ function checkForToolLoop(message: ChatMessages): boolean { return hasDuplicates; } // TODO: add props for config chat + +export function chatModeToLspMode(mode?: ToolUse) { + if (mode === "agent") return "AGENT"; + if (mode === "quick") return "NOTOOLS"; + return "EXPLORE"; +} export const chatAskQuestionThunk = createAppAsyncThunk< unknown, { messages: ChatMessages; chatId: string; tools: ToolCommand[] | null; + mode?: string; // used for actions // TODO: make a separate function for this... and it'll need to be saved. } ->("chatThread/sendChat", ({ messages, chatId, tools }, thunkAPI) => { +>("chatThread/sendChat", ({ messages, chatId, tools, mode }, thunkAPI) => { const state = thunkAPI.getState(); const thread = @@ -217,12 +225,22 @@ export const chatAskQuestionThunk = createAppAsyncThunk< ? state.chat.thread : null; - const isConfig = !!thread?.integration; + // TODO: stops the stream. + // const onlyDeterministicMessages = + // checkForToolLoop(messages) || !messages.some(isSystemMessage); const onlyDeterministicMessages = checkForToolLoop(messages); const messagesForLsp = formatMessagesForLsp(messages); + const maybeMode = mode + ? mode + : thread?.integration + ? "CONFIGURE" + : thread?.tool_use + ? chatModeToLspMode(thread.tool_use) + : chatModeToLspMode(state.chat.tool_use); + return sendChat({ messages: messagesForLsp, model: state.chat.thread.model, @@ -233,7 +251,8 @@ export const chatAskQuestionThunk = createAppAsyncThunk< apiKey: state.config.apiKey, port: state.config.lspPort, onlyDeterministicMessages, - isConfig, + integration: thread?.integration, + mode: maybeMode, }) .then((response) => { if (!response.ok) { diff --git a/src/features/Chat/Thread/reducer.ts b/src/features/Chat/Thread/reducer.ts index 8a6c40ce..c4a163dd 100644 --- a/src/features/Chat/Thread/reducer.ts +++ b/src/features/Chat/Thread/reducer.ts @@ -1,5 +1,5 @@ import { createReducer } from "@reduxjs/toolkit"; -import { Chat, ChatThread, ToolUse } from "./types"; +import { Chat, ChatThread, IntegrationMeta, ToolUse } from "./types"; import { v4 as uuidv4 } from "uuid"; import { chatResponse, chatAskedQuestion } from "."; import { @@ -23,7 +23,7 @@ import { formatChatResponse } from "./utils"; const createChatThread = ( tool_use: ToolUse, - integration?: { name: string; path: string } | null, + integration?: IntegrationMeta | null, ): ChatThread => { const chat: ChatThread = { id: uuidv4(), @@ -38,7 +38,7 @@ const createChatThread = ( const createInitialState = ( tool_use: ToolUse = "explore", - integration?: { name: string; path: string } | null, + integration?: IntegrationMeta | null, ): Chat => { return { streaming: false, @@ -194,6 +194,7 @@ export const chatReducer = createReducer(initialState, (builder) => { builder.addCase(newIntegrationChat, (state, action) => { // TODO: find out about tool use + // TODO: should be CONFIGURE ? const next = createInitialState("agent", action.payload.integration); next.thread.integration = action.payload.integration; next.thread.messages = action.payload.messages; @@ -204,9 +205,6 @@ export const chatReducer = createReducer(initialState, (builder) => { if (state.streaming) { next.cache[state.thread.id] = { ...state.thread, read: false }; } - // TBD: this might not be needed. - // next.thread.model = state.thread.model; - // next.system_prompt = state.system_prompt; return next; }); diff --git a/src/features/Chat/Thread/types.ts b/src/features/Chat/Thread/types.ts index 62eb743f..aab92b2a 100644 --- a/src/features/Chat/Thread/types.ts +++ b/src/features/Chat/Thread/types.ts @@ -2,6 +2,7 @@ import { SystemPrompts } from "../../../services/refact/prompts"; import { ChatMessages } from "../../../services/refact/types"; import { parseOrElse } from "../../../utils/parseOrElse"; +export type IntegrationMeta = { name: string; path: string; project: string }; export type ChatThread = { id: string; messages: ChatMessages; @@ -12,7 +13,7 @@ export type ChatThread = { tool_use?: ToolUse; read?: boolean; isTitleGenerated?: boolean; - integration?: { name: string; path: string } | null; + integration?: IntegrationMeta | null; }; export type ToolUse = "quick" | "explore" | "agent"; diff --git a/src/features/Chat/Thread/utils.ts b/src/features/Chat/Thread/utils.ts index 260a41ff..6455afd6 100644 --- a/src/features/Chat/Thread/utils.ts +++ b/src/features/Chat/Thread/utils.ts @@ -23,6 +23,7 @@ import { isPlainTextResponse, isSubchatContextFileResponse, isSubchatResponse, + isSystemResponse, isToolCallDelta, isToolContent, isToolMessage, @@ -182,6 +183,11 @@ export function formatChatResponse( return [...messages, { role: response.role, content: response.content }]; } + // system messages go to the front + if (isSystemResponse(response)) { + return [{ role: response.role, content: response.content }, ...messages]; + } + if (!isChatResponseChoice(response)) { // console.log("Not a good response"); // console.log(response); @@ -452,13 +458,6 @@ export function formatMessagesForChat( return acc.concat(message as unknown as ToolMessage); } - if ( - message.role === "cd_instruction" && - typeof message.content === "string" - ) { - return acc.concat({ role: message.role, content: message.content }); - } - if ( message.role === "diff" && Array.isArray(message.content) && diff --git a/src/features/Integrations/renderIntegrationFormField.tsx b/src/features/Integrations/renderIntegrationFormField.tsx index f62c14ea..60852dab 100644 --- a/src/features/Integrations/renderIntegrationFormField.tsx +++ b/src/features/Integrations/renderIntegrationFormField.tsx @@ -52,12 +52,14 @@ export const renderIntegrationFormField = ({ integrationName, integrationPath, isFieldVisible = true, + integrationProject, }: { fieldKey: string; values: Integration["integr_values"]; field: IntegrationField>; integrationName: string; integrationPath: string; + integrationProject: string; isFieldVisible?: boolean; }) => { const [f_type_raw, f_size] = field.f_type.toString().split("_"); @@ -123,6 +125,7 @@ export const renderIntegrationFormField = ({ smartlink={smartlink} integrationName={integrationName} integrationPath={integrationPath} + integrationProject={integrationProject} /> ))} diff --git a/src/features/Pages/pagesSlice.ts b/src/features/Pages/pagesSlice.ts index a95b4245..d4a19e65 100644 --- a/src/features/Pages/pagesSlice.ts +++ b/src/features/Pages/pagesSlice.ts @@ -58,6 +58,7 @@ export interface IntegrationsSetupPage { name: "integrations page"; projectPath?: string; integrationName?: string; + integrationPath?: string; } export type Page = diff --git a/src/hooks/useSendChatRequest.ts b/src/hooks/useSendChatRequest.ts index 890f5665..39c83167 100644 --- a/src/hooks/useSendChatRequest.ts +++ b/src/hooks/useSendChatRequest.ts @@ -73,7 +73,7 @@ export const useSendChatRequest = () => { }, [currentMessages, systemPrompt]); const sendMessages = useCallback( - async (messages: ChatMessages) => { + async (messages: ChatMessages, mode?: string) => { let tools = await triggerGetTools(undefined).unwrap(); // TODO: save tool use to state.chat // if (toolUse && isToolUse(toolUse)) { @@ -112,6 +112,7 @@ export const useSendChatRequest = () => { messages, tools, chatId, + mode, }); const dispatchedAction = dispatch(action); @@ -156,11 +157,11 @@ export const useSendChatRequest = () => { ); const submit = useCallback( - (question: string) => { + (question: string, mode?: string) => { // const message: ChatMessage = { role: "user", content: question }; const message: UserMessage = maybeAddImagesToQuestion(question); const messages = messagesWithSystemPrompt.concat(message); - void sendMessages(messages); + void sendMessages(messages, mode); }, [maybeAddImagesToQuestion, messagesWithSystemPrompt, sendMessages], ); diff --git a/src/services/refact/chat.ts b/src/services/refact/chat.ts index 6c4d4cf9..788ee913 100644 --- a/src/services/refact/chat.ts +++ b/src/services/refact/chat.ts @@ -1,3 +1,4 @@ +import { IntegrationMeta } from "../../features/Chat"; import { CHAT_URL } from "./consts"; import { ToolCommand } from "./tools"; import { ChatRole, ToolCall, ToolResult, UserMessage } from "./types"; @@ -43,7 +44,9 @@ type SendChatArgs = { tools: ToolCommand[] | null; port?: number; apiKey?: string | null; - isConfig?: boolean; + // isConfig?: boolean; + integration?: IntegrationMeta | null; + mode?: string; // used for chat actions } & StreamArgs; type GetChatTitleArgs = { @@ -110,7 +113,9 @@ export async function sendChat({ tools, port = 8001, apiKey, - isConfig = false, + // isConfig = false, + integration, + mode, }: SendChatArgs): Promise { // const toolsResponse = await getAvailableTools(); @@ -132,7 +137,15 @@ export async function sendChat({ tools, max_tokens: 2048, only_deterministic_messages, - chat_id, + // chat_id, + meta: { + chat_id, + // chat_remote, + // TODO: pass this through + chat_mode: mode ?? "EXPLORE", + // chat_mode: "EXPLORE", // NOTOOLS, EXPLORE, AGENT, CONFIGURE, PROJECTSUMMARY, + ...(integration?.path ? { current_config_file: integration.path } : {}), + }, }); // const apiKey = getApiKey(); @@ -142,7 +155,7 @@ export async function sendChat({ }; const url = `http://127.0.0.1:${port}${ - isConfig ? "/v1/chat-configuration" : CHAT_URL + integration ? "/v1/chat-configuration" : CHAT_URL }`; return fetch(url, { diff --git a/src/services/refact/consts.ts b/src/services/refact/consts.ts index 7a7c1781..8ff71628 100644 --- a/src/services/refact/consts.ts +++ b/src/services/refact/consts.ts @@ -16,6 +16,7 @@ export const DOCUMENTATION_REMOVE = `/v1/docs-remove`; export const PING_URL = `/v1/ping`; export const PATCH_URL = `/v1/patch-single-file-from-ticket`; export const APPLY_ALL_URL = "/v1/patch-apply-all"; +export const CHAT_LINKS_URL = "/v1/links"; // Integrations export const INTEGRATIONS_URL = "/v1/integrations"; export const INTEGRATION_GET_URL = "/v1/integration-get"; diff --git a/src/services/refact/index.ts b/src/services/refact/index.ts index b87dbbfd..478cf21f 100644 --- a/src/services/refact/index.ts +++ b/src/services/refact/index.ts @@ -9,5 +9,6 @@ export * from "./types"; export * from "./diffs"; export * from "./path"; export * from "./ping"; +export * from "./links"; export * from "./integrations"; export * from "./docker"; diff --git a/src/services/refact/links.ts b/src/services/refact/links.ts new file mode 100644 index 00000000..9cb331ad --- /dev/null +++ b/src/services/refact/links.ts @@ -0,0 +1,95 @@ +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; +import { RootState } from "../../app/store"; +import { ChatMessages } from "./types"; +import { formatMessagesForLsp } from "../../features/Chat/Thread/utils"; +import { CHAT_LINKS_URL } from "./consts"; + +// goto: can be an integration file to open in settings, a file to open in an idea or a global integration. +export type ChatLink = + | { text: string; goto: string; action: string } + | { text: string; goto: string /* action: undefined */ } + | { text: string; /* goto: undefined; */ action: string } + | { text: string; goto: string; action: "go-to" }; + +function isChatLink(json: unknown): json is ChatLink { + if (!json || typeof json !== "object") return false; + + if (!("text" in json)) return false; + if (typeof json.text !== "string") return false; + + if ("goto" in json && typeof json.goto === "string") return true; + + if ("action" in json && typeof json.action === "string") return true; + + return false; +} + +export type LinksForChatResponse = { + links: ChatLink[]; +}; + +export type LinksApiRequest = { + chat_id: string; + messages: ChatMessages; + model: string; + mode?: string; + current_config_file?: string; +}; + +function isLinksForChatResponse(json: unknown): json is LinksForChatResponse { + if (!json || typeof json !== "object") return false; + if (!("links" in json)) return false; + if (!Array.isArray(json.links)) return false; + return json.links.every(isChatLink); +} + +export const linksApi = createApi({ + reducerPath: "linksApi", + baseQuery: fetchBaseQuery({ + prepareHeaders: (headers, { getState }) => { + const token = (getState() as RootState).config.apiKey; + if (token) { + headers.set("Authorization", `Bearer ${token}`); + } + return headers; + }, + }), + endpoints: (builder) => ({ + getLinksForChat: builder.mutation({ + async queryFn(args, _api, extraOptions, baseQuery) { + const messageFotLsp = formatMessagesForLsp(args.messages); + + const response = await baseQuery({ + ...extraOptions, + method: "POST", + url: CHAT_LINKS_URL, + body: { + meta: { + chat_id: args.chat_id, + current: args.current_config_file, + chat_mode: args.mode, + }, + messages: messageFotLsp, + model_name: args.model, + }, + }); + + if (response.error) { + return { error: response.error }; + } + + if (!isLinksForChatResponse(response.data)) { + return { + error: { + error: "Invalid response for chat links", + data: response.data, + status: "CUSTOM_ERROR", + }, + }; + } + return { data: response.data }; + }, + }), + }), + // refetchOnMountOrArgChange: true, +}); diff --git a/src/services/refact/types.ts b/src/services/refact/types.ts index 39a9f50a..28199030 100644 --- a/src/services/refact/types.ts +++ b/src/services/refact/types.ts @@ -249,6 +249,12 @@ export function isDiffMessage(message: ChatMessage): message is DiffMessage { return message.role === "diff"; } +export function isSystemMessage( + message: ChatMessage, +): message is SystemMessage { + return message.role === "system"; +} + export function isToolCallMessage( message: ChatMessage, ): message is ToolCallMessage { @@ -490,6 +496,13 @@ export function isSubchatResponse(json: unknown): json is SubchatResponse { return true; } +export function isSystemResponse(json: unknown): json is SystemMessage { + if (!json) return false; + if (typeof json !== "object") return false; + if (!("role" in json)) return false; + return json.role === "system"; +} + export function isCDInstructionResponse( json: unknown, ): json is CDInstructionMessage {