Skip to content

Commit

Permalink
wip(command completion): get the next commands
Browse files Browse the repository at this point in the history
todo: force the combobox open when there are commands
  • Loading branch information
MarcMcIntosh committed Feb 1, 2024
1 parent 74e0735 commit 6558f2e
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 6 deletions.
44 changes: 39 additions & 5 deletions src/components/ChatForm/ChatForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const ChatForm: React.FC<{
hasContextFile: boolean;
commands: ChatState["rag_commands"];
attachFile: ChatState["active_file"];
setSelectedCommand: (command: string) => void;
requestCommandsCompletion: (
query: string,
cursor: number,
Expand All @@ -76,7 +77,8 @@ export const ChatForm: React.FC<{
hasContextFile,
commands,
attachFile,
// requestCommandsCompletion,
requestCommandsCompletion,
setSelectedCommand,
}) => {
const [value, setValue] = React.useState("");

Expand All @@ -91,6 +93,28 @@ export const ChatForm: React.FC<{
};

const handleEnter = useOnPressedEnter(handleSubmit);

const handleChange = (command: React.SetStateAction<string>) => {
setValue(command);

const str = typeof command === "function" ? command(value) : command;

if (
str.endsWith(" ") &&
commands.available_commands.includes(str.slice(0, -1))
) {
setSelectedCommand(str);
} else {
setSelectedCommand("");
}

// TODO: get the cursor position
// Does order matter here?
if (str.startsWith("@")) {
requestCommandsCompletion(str, str.length);
}
// set selected command if value ends with space and is in commands
};
if (error) {
return (
<ErrorCallout mt="2" onClick={clearError} timeout={null}>
Expand All @@ -99,6 +123,8 @@ export const ChatForm: React.FC<{
);
}

// console.log({ commands });

return (
<Box mt="1" position="relative">
{!isOnline && <Callout type="info">Offline</Callout>}
Expand Down Expand Up @@ -136,11 +162,19 @@ export const ChatForm: React.FC<{
onSubmit={() => handleSubmit()}
>
<ComboBox
commands={commands.available_commands}
// maybe add a ref for cursor position?
commands={commands.available_commands.map(
(c) => commands.selected_command + c,
)}
value={value}
onChange={setValue}
onSubmit={handleEnter}
placeholder={commands.length > 0 ? "Type @ for commands" : ""}
onChange={handleChange}
onSubmit={(event) => {
// console.log("submit", event);
handleEnter(event);
}}
placeholder={
commands.available_commands.length > 0 ? "Type @ for commands" : ""
}
render={(props) => <TextArea disabled={isStreaming} {...props} />}
/>
<Flex gap="2" className={styles.buttonGroup}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ComboBox/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const Popover: React.FC<
</Box>
);
};

// TODO: force this open when there are commands
export const ComboBox: React.FC<{
commands: string[];
onChange: React.Dispatch<React.SetStateAction<string>>;
Expand Down
14 changes: 14 additions & 0 deletions src/events/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export enum EVENT_NAMES_TO_CHAT {
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",
}

export type ChatThread = {
Expand Down Expand Up @@ -216,6 +217,19 @@ export function isReceiveAtCommandPreview(
if (!isActionToChat(action)) return false;
return action.type === EVENT_NAMES_TO_CHAT.RECEIVE_AT_COMMAND_PREVIEW;
}

export interface SetSelectedAtCommand extends ActionToChat {
type: EVENT_NAMES_TO_CHAT.SET_SELECTED_AT_COMMAND;
payload: { id: string; command: string };
}

export function isSetSelectedAtCommand(
action: unknown,
): action is SetSelectedAtCommand {
if (!isActionToChat(action)) return false;
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 };
Expand Down
4 changes: 4 additions & 0 deletions src/features/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export const Chat: React.FC<{ style?: React.CSSProperties }> = (props) => {
sendReadyMessage,
handleNewFileClick,
handlePasteDiffClick,
requestCommandsCompletion,
setSelectedCommand,
} = useEventBusForChat();

const maybeSendToSideBar =
Expand Down Expand Up @@ -113,6 +115,8 @@ export const Chat: React.FC<{ style?: React.CSSProperties }> = (props) => {
handleContextFile={handleContextFile}
hasContextFile={hasContextFile}
commands={state.rag_commands}
requestCommandsCompletion={requestCommandsCompletion}
setSelectedCommand={setSelectedCommand}
onClose={maybeSendToSideBar}
attachFile={state.active_file}
/>
Expand Down
23 changes: 23 additions & 0 deletions src/hooks/useEventBusForChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {
ReadyMessage,
RequestAtCommandCompletion,
isReceiveAtCommandCompletion,
SetSelectedAtCommand,
isSetSelectedAtCommand,
} from "../events";
import { useConfig } from "../contexts/config-context";
import { usePostMessage } from "./usePostMessage";
Expand Down Expand Up @@ -273,6 +275,18 @@ function reducer(state: ChatState, action: ActionToChat): ChatState {
rag_commands: {
...state.rag_commands,
available_commands: action.payload.completions,
// arguments: args,
is_cmd_executable: action.payload.is_cmd_executable,
},
};
}

if (isThisChat && isSetSelectedAtCommand(action)) {
return {
...state,
rag_commands: {
...state.rag_commands,
selected_command: action.payload.command,
},
};
}
Expand Down Expand Up @@ -538,6 +552,14 @@ export const useEventBusForChat = () => {
postMessage(action);
}

function setSelectedCommand(command: string) {
const action: SetSelectedAtCommand = {
type: EVENT_NAMES_TO_CHAT.SET_SELECTED_AT_COMMAND,
payload: { id: state.chat.id, command },
};
postMessage(action);
}

useEffectOnce(() => {
requestCommandsCompletion("@", 1);
});
Expand All @@ -558,5 +580,6 @@ export const useEventBusForChat = () => {
handleNewFileClick,
handlePasteDiffClick,
requestCommandsCompletion,
setSelectedCommand,
};
};
115 changes: 115 additions & 0 deletions src/services/refact.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const CHAT_URL = `/v1/chat`;
const CAPS_URL = `/v1/caps`;
const AT_COMMAND_COMPLETION = "/v1/at-command-completion";
const AT_COMMAND_PREVIEW = "/v1/at-command-preview";

export type ChatRole = "user" | "assistant" | "context_file" | "system";

Expand Down Expand Up @@ -197,3 +199,116 @@ export type CapsResponse = {
tokenizer_rewrite_path: Record<string, unknown>;
chat_rag_functions?: string[];
};

interface Replace {
0: number;
1: number;
}

export type CommandCompletionResponse = {
completions: string[];
replace: Replace;
is_cmd_executable: false;
};

function isCommandCompletionResponse(
json: unknown,
): json is CommandCompletionResponse {
if (!json) return false;
if (typeof json !== "object") return false;
if (!("completions" in json)) return false;
if (!("replace" in json)) return false;
if (!("is_cmd_executable" in json)) return false;
return true;
}

export async function getAtCommandCompletion(
query: string,
cursor: number,
number: number,
lspUrl?: string,
): Promise<CommandCompletionResponse> {
const completionEndpoint = lspUrl
? `${lspUrl.replace(/\/*$/, "")}${AT_COMMAND_COMPLETION}`
: AT_COMMAND_COMPLETION;

const response = await fetch(completionEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ query, cursor, top_n: number }),
});

if (!response.ok) {
throw new Error(response.statusText);
}

const json: unknown = await response.json();
if (!isCommandCompletionResponse(json)) {
throw new Error("Invalid response from completion");
}

return json;
}

export type CommandPreviewResponse = {
messages: {
choices: {
delta: {
content: {
file_content: string;
file_name: string;
};
role: "content_file";
};
finish_reason: null;
index: number;
}[];
}[];
};

function isCommandPreviewResponse(
json: unknown,
): json is CommandPreviewResponse {
if (!json) return false;
if (typeof json !== "object") return false;
if (!("messages" in json)) return false;
if (!Array.isArray(json.messages)) return false;
if (!("choices" in json.messages[0])) return false;
return true;
}

export async function getAtCommandPreview(
query: string,
lspUrl?: string,
): Promise<{ file_name: string; file_content: string }> {
const previewEndpoint = lspUrl
? `${lspUrl.replace(/\/*$/, "")}${AT_COMMAND_PREVIEW}`
: AT_COMMAND_PREVIEW;

const response = await fetch(previewEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
redirect: "follow",
cache: "no-cache",
referrer: "no-referrer",
credentials: "same-origin",
body: JSON.stringify({ query }),
});

if (!response.ok) {
throw new Error(response.statusText);
}

const json: unknown = await response.json();

if (!isCommandPreviewResponse(json)) {
throw new Error("Invalid response from command preview");
}
const { file_name, file_content } = json.messages[0].choices[0].delta.content;

return { file_name, file_content };
}

0 comments on commit 6558f2e

Please sign in to comment.