From 45c7790a0561d2b0d84542b33cd121525541fc9f Mon Sep 17 00:00:00 2001 From: Marc McIntosh Date: Sat, 24 Feb 2024 14:41:25 +0100 Subject: [PATCH] feat(attach file): attach file using @ command --- README.md | 18 +---- TODO.md | 2 + src/components/ChatForm/CharForm.test.tsx | 1 - src/components/ChatForm/ChatForm.stories.tsx | 1 - src/components/ChatForm/ChatForm.tsx | 25 +++++-- src/events/chat.ts | 53 ------------- src/features/Chat.tsx | 6 +- src/hooks/useEventBusForChat.ts | 79 -------------------- src/hooks/useEventBusForHost.ts | 50 ------------- 9 files changed, 24 insertions(+), 211 deletions(-) diff --git a/README.md b/README.md index b981943d..121bc2a5 100644 --- a/README.md +++ b/README.md @@ -238,20 +238,12 @@ interface ActiveFileInfo extends ActionToChat { payload: { id: string; name: string; can_paste: boolean }; } -/** - * This message is sent from the host to the chat if the active file should be attached, this is used when adding snippets to the user input. - */ -interface ToggleActiveFile extends ActionToChat { - type: EVENT_NAMES_TO_CHAT.TOGGLE_ACTIVE_FILE; // = "chat_toggle_active_file" - payload: { id: string; attach_file: boolean }; -} - /** * This message is sent from the host to the chat to set the selected snippet. */ interface ChatSetSelectedSnippet extends ActionToChat { type: EVENT_NAMES_TO_CHAT.SET_SELECTED_SNIPPET; // = "chat_set_selected_command" - payload: { id: string; snippet: string; language: string }; + payload: { id: string; snippet: { code: string; language: string } }; } /** @@ -285,14 +277,6 @@ interface PasteDiffFromChat extends ActionFromChat { type: EVENT_NAMES_FROM_CHAT.PASTE_DIFF; // = "chat_paste_diff" payload: { id: string; content: string }; } - -/** - * This message is used to attach context files from the host to the chat. - */ -interface ReceiveContextFile extends ActionToChat { - type: EVENT_NAMES_TO_CHAT.RECEIVE_FILES; // = "receive_context_file" - payload: { id: string; files: ChatContextFile[] }; -} ``` ### Data types in the events diff --git a/TODO.md b/TODO.md index 20bdd3c5..2b190ab4 100644 --- a/TODO.md +++ b/TODO.md @@ -118,6 +118,8 @@ [x] limit the size of undo / redo history [x] fix re-attaching files on retry [x] undo redo, holding ctrl keeps the box open until the user releases it +[x] attach file with @ command, +[x] bug: add text, add file go back and edit the text fixed by prepending the command to the value ### EVENTS TODO FOR IDEs diff --git a/src/components/ChatForm/CharForm.test.tsx b/src/components/ChatForm/CharForm.test.tsx index 4388585c..ebfa834b 100644 --- a/src/components/ChatForm/CharForm.test.tsx +++ b/src/components/ChatForm/CharForm.test.tsx @@ -18,7 +18,6 @@ const App: React.FC> = (props) => { error: "", clearError: noop, canChangeModel: false, - handleContextFile: noop, hasContextFile: false, commands: { available_commands: [], diff --git a/src/components/ChatForm/ChatForm.stories.tsx b/src/components/ChatForm/ChatForm.stories.tsx index cea34bd7..5ad75b5b 100644 --- a/src/components/ChatForm/ChatForm.stories.tsx +++ b/src/components/ChatForm/ChatForm.stories.tsx @@ -65,7 +65,6 @@ const meta = { clearError: noop, canChangeModel: true, hasContextFile: false, - handleContextFile: noop, commands: { available_commands: testCommands, selected_command: "", diff --git a/src/components/ChatForm/ChatForm.tsx b/src/components/ChatForm/ChatForm.tsx index 09434414..63063dfd 100644 --- a/src/components/ChatForm/ChatForm.tsx +++ b/src/components/ChatForm/ChatForm.tsx @@ -20,6 +20,7 @@ import { ComboBox, type ComboBoxProps } from "../ComboBox"; import type { ChatState } from "../../hooks"; import { ChatContextFile } from "../../services/refact"; import { FilesPreview } from "./FilesPreview"; +import { useConfig } from "../../contexts/config-context"; const CapsSelect: React.FC<{ value: string; @@ -53,10 +54,9 @@ export type ChatFormProps = { canChangeModel: boolean; isStreaming: boolean; onStopStreaming: () => void; - handleContextFile: () => void; - hasContextFile: boolean; commands: ChatState["rag_commands"]; attachFile: ChatState["active_file"]; + hasContextFile: boolean; requestCommandsCompletion: ComboBoxProps["requestCommandsCompletion"]; setSelectedCommand: (command: string) => void; filesInPreview: ChatContextFile[]; @@ -77,7 +77,6 @@ export const ChatForm: React.FC = ({ canChangeModel, isStreaming, onStopStreaming, - handleContextFile, hasContextFile, commands, attachFile, @@ -90,7 +89,9 @@ export const ChatForm: React.FC = ({ }) => { const [value, setValue] = React.useState(""); const [snippetAdded, setSnippetAdded] = React.useState(false); + const config = useConfig(); + // TODO: this won't update the value in the text area useEffect(() => { if (!snippetAdded && selectedSnippet.code) { setValue( @@ -128,14 +129,26 @@ export const ChatForm: React.FC = ({ ); } + const checked = value.includes(`@file ${attachFile.name}`); + return ( {!isOnline && Offline} - {canChangeModel && ( + {config.host !== "web" && !hasContextFile && ( + setValue((preValue) => { + const command = `@file ${attachFile.name}${ + value.length > 0 ? "\n" : "" + }`; + if (checked) { + return preValue.replace(command, ""); + } + return `${command}${preValue}`; + }) + } + checked={checked} /> )} diff --git a/src/events/chat.ts b/src/events/chat.ts index 25073d44..304707ae 100644 --- a/src/events/chat.ts +++ b/src/events/chat.ts @@ -3,7 +3,6 @@ import { ChatResponse, CapsResponse, isCapsResponse, - ChatContextFile, CommandCompletionResponse, ChatContextFileMessage, } from "../services/refact"; @@ -13,7 +12,6 @@ export enum EVENT_NAMES_FROM_CHAT { ASK_QUESTION = "chat_question", REQUEST_CAPS = "chat_request_caps", STOP_STREAMING = "chat_stop_streaming", - REQUEST_FILES = "chat_request_for_file", BACK_FROM_CHAT = "chat_back_from_chat", OPEN_IN_CHAT_IN_TAB = "open_chat_in_new_tab", SEND_TO_SIDE_BAR = "chat_send_to_sidebar", @@ -35,10 +33,7 @@ export enum EVENT_NAMES_TO_CHAT { RECEIVE_CAPS_ERROR = "receive_caps_error", SET_CHAT_MODEL = "chat_set_chat_model", SET_DISABLE_CHAT = "set_disable_chat", - RECEIVE_FILES = "receive_context_file", - REMOVE_FILES = "remove_context_file", ACTIVE_FILE_INFO = "chat_active_file_info", - TOGGLE_ACTIVE_FILE = "chat_toggle_active_file", RECEIVE_AT_COMMAND_COMPLETION = "chat_receive_at_command_completion", RECEIVE_AT_COMMAND_PREVIEW = "chat_receive_at_command_preview", SET_SELECTED_AT_COMMAND = "chat_set_selected_command", @@ -126,18 +121,6 @@ export function isPasteDiffFromChat( return action.type === EVENT_NAMES_FROM_CHAT.PASTE_DIFF; } -export interface RequestForFileFromChat extends ActionFromChat { - type: EVENT_NAMES_FROM_CHAT.REQUEST_FILES; - payload: { id: string }; -} - -export function isRequestForFileFromChat( - action: unknown, -): action is RequestForFileFromChat { - if (!isActionFromChat(action)) return false; - return action.type === EVENT_NAMES_FROM_CHAT.REQUEST_FILES; -} - export interface QuestionFromChat extends ActionFromChat { type: EVENT_NAMES_FROM_CHAT.ASK_QUESTION; payload: ChatThread; @@ -235,17 +218,6 @@ export function isSetSelectedAtCommand( return action.type === EVENT_NAMES_TO_CHAT.SET_SELECTED_AT_COMMAND; } -export interface ToggleActiveFile extends ActionToChat { - type: EVENT_NAMES_TO_CHAT.TOGGLE_ACTIVE_FILE; - payload: { id: string; attach_file: boolean }; -} - -export function isToggleActiveFile( - action: unknown, -): action is ToggleActiveFile { - if (!isActionToChat(action)) return false; - return action.type === EVENT_NAMES_TO_CHAT.TOGGLE_ACTIVE_FILE; -} export interface ActiveFileInfo extends ActionToChat { type: EVENT_NAMES_TO_CHAT.ACTIVE_FILE_INFO; payload: { id: string; name: string; can_paste: boolean }; @@ -256,31 +228,6 @@ export function isActiveFileInfo(action: unknown): action is ActiveFileInfo { return action.type === EVENT_NAMES_TO_CHAT.ACTIVE_FILE_INFO; } -export interface ReceiveContextFile extends ActionToChat { - type: EVENT_NAMES_TO_CHAT.RECEIVE_FILES; - payload: { - id: string; - files: ChatContextFile[]; - }; -} - -export function isReceiveContextFile( - action: unknown, -): action is ReceiveContextFile { - if (!isActionToChat(action)) return false; - return action.type === EVENT_NAMES_TO_CHAT.RECEIVE_FILES; -} - -export interface RemoveContextFile extends ActionToChat { - type: EVENT_NAMES_TO_CHAT.REMOVE_FILES; - payload: { id: string }; -} - -export function isRemoveContext(action: unknown): action is RemoveContextFile { - if (!isActionToChat(action)) return false; - return action.type === EVENT_NAMES_TO_CHAT.REMOVE_FILES; -} - export interface SetChatDisable extends ActionToChat { type: EVENT_NAMES_TO_CHAT.SET_DISABLE_CHAT; payload: { id: string; disable: boolean }; diff --git a/src/features/Chat.tsx b/src/features/Chat.tsx index 09870424..903d17d3 100644 --- a/src/features/Chat.tsx +++ b/src/features/Chat.tsx @@ -23,14 +23,13 @@ export const Chat: React.FC<{ style?: React.CSSProperties }> = (props) => { clearError, setChatModel, stopStreaming, - handleContextFile, - hasContextFile, backFromChat, openChatInNewTab, sendToSideBar, sendReadyMessage, handleNewFileClick, handlePasteDiffClick, + hasContextFile, requestCommandsCompletion, setSelectedCommand, removePreviewFileByName, @@ -118,9 +117,8 @@ export const Chat: React.FC<{ style?: React.CSSProperties }> = (props) => { onSetChatModel={setChatModel} caps={state.caps} onStopStreaming={stopStreaming} - handleContextFile={handleContextFile} - hasContextFile={hasContextFile} commands={state.rag_commands} + hasContextFile={hasContextFile} requestCommandsCompletion={requestCommandsCompletion} setSelectedCommand={setSelectedCommand} onClose={maybeSendToSideBar} diff --git a/src/hooks/useEventBusForChat.ts b/src/hooks/useEventBusForChat.ts index c817a77e..a6a7dd19 100644 --- a/src/hooks/useEventBusForChat.ts +++ b/src/hooks/useEventBusForChat.ts @@ -25,12 +25,7 @@ import { isChatReceiveCapsError, isSetChatModel, isSetDisableChat, - isReceiveContextFile, - isRequestForFileFromChat, - isRemoveContext, isActiveFileInfo, - isToggleActiveFile, - type ToggleActiveFile, type NewFileFromChat, type PasteDiffFromChat, type ReadyMessage, @@ -48,7 +43,6 @@ import { setPreviousMessagesLength, type Snippet, } from "../events"; -import { useConfig } from "../contexts/config-context"; import { usePostMessage } from "./usePostMessage"; import { useDebounceCallback } from "usehooks-ts"; @@ -245,41 +239,6 @@ function reducer(state: ChatState, action: ActionToChat): ChatState { }; } - if (isThisChat && isRequestForFileFromChat(action)) { - return { - ...state, - waiting_for_response: true, - }; - } - - if (isThisChat && isReceiveContextFile(action)) { - return { - ...state, - waiting_for_response: false, - chat: { - ...state.chat, - messages: [ - ["context_file", action.payload.files], - ...state.chat.messages, - ], - }, - }; - } - - if (isThisChat && isRemoveContext(action)) { - const messages = state.chat.messages.filter( - (message) => !isChatContextFileMessage(message), - ); - - return { - ...state, - chat: { - ...state.chat, - messages, - }, - }; - } - if (isThisChat && isActiveFileInfo(action)) { const { name, can_paste } = action.payload; return { @@ -292,16 +251,6 @@ function reducer(state: ChatState, action: ActionToChat): ChatState { }; } - if (isThisChat && isToggleActiveFile(action)) { - return { - ...state, - active_file: { - ...state.active_file, - attach: action.payload.attach_file, - }, - }; - } - if (isThisChat && isReceiveAtCommandCompletion(action)) { const selectedCommand = state.rag_commands.selected_command; const availableCommands = selectedCommand @@ -450,7 +399,6 @@ function createInitialState(): ChatState { const initialState = createInitialState(); // Maybe use context to avoid prop drilling? export const useEventBusForChat = () => { - const config = useConfig(); const [state, dispatch] = useReducer(reducer, initialState); const postMessage = usePostMessage(); @@ -580,32 +528,6 @@ export const useEventBusForChat = () => { isChatContextFileMessage(message), ); - function handleContextFileForWeb() { - if (hasContextFile) { - dispatch({ - type: EVENT_NAMES_TO_CHAT.REMOVE_FILES, - payload: { id: state.chat.id }, - }); - } else { - postMessage({ - type: EVENT_NAMES_FROM_CHAT.REQUEST_FILES, - payload: { id: state.chat.id }, - }); - } - } - - function handleContextFile(toggle?: boolean) { - if (config.host === "web") { - handleContextFileForWeb(); - } else { - const action: ToggleActiveFile = { - type: EVENT_NAMES_TO_CHAT.TOGGLE_ACTIVE_FILE, - payload: { id: state.chat.id, attach_file: !!toggle }, - }; - dispatch(action); - } - } - function backFromChat() { postMessage({ type: EVENT_NAMES_FROM_CHAT.BACK_FROM_CHAT, @@ -716,7 +638,6 @@ export const useEventBusForChat = () => { clearError, setChatModel, stopStreaming, - handleContextFile, hasContextFile, backFromChat, openChatInNewTab, diff --git a/src/hooks/useEventBusForHost.ts b/src/hooks/useEventBusForHost.ts index 885b6ca2..374f12b5 100644 --- a/src/hooks/useEventBusForHost.ts +++ b/src/hooks/useEventBusForHost.ts @@ -2,7 +2,6 @@ import { useEffect, useRef } from "react"; import { sendChat, getCaps, - ChatContextFile, getAtCommandCompletion, getAtCommandPreview, isDetailMessage, @@ -15,7 +14,6 @@ import { isSaveChatFromChat, isRequestCapsFromChat, isStopStreamingFromChat, - isRequestForFileFromChat, isRequestAtCommandCompletion, ReceiveAtCommandCompletion, ReceiveAtCommandPreview, @@ -84,54 +82,6 @@ export function useEventBusForHost() { }); } - if (isRequestForFileFromChat(event.data)) { - const { payload } = event.data; - - window - .showOpenFilePicker({ multiple: true }) - .then(async (fileHandlers) => { - const promises = fileHandlers.map(async (fileHandler) => { - const file = await fileHandler.getFile(); - const content = await file.text(); - const messageInChat: ChatContextFile = { - file_name: fileHandler.name, - file_content: content, - line1: 1, - line2: content.split("\n").length + 1, - }; - return messageInChat; - }); - - const files = await Promise.all(promises); - window.postMessage({ - type: EVENT_NAMES_TO_CHAT.RECEIVE_FILES, - payload: { - id: payload.id, - files, - }, - }); - }) - .catch((error: Error) => { - if (error instanceof DOMException && error.name === "AbortError") { - return; - } - // eslint-disable-next-line no-console - console.error(error); - - // TODO: add specific error type for this case - window.postMessage( - { - type: EVENT_NAMES_TO_CHAT.ERROR_STREAMING, - payload: { - id: payload.id, - message: error.message || "error attaching file", - }, - }, - "*", - ); - }); - } - if (isRequestAtCommandCompletion(event.data)) { const { id, query, cursor, number, trigger } = event.data.payload; // getAtCommandCompletion(query, cursor, number, lspUrl)