Skip to content
4 changes: 2 additions & 2 deletions .github/workflows/agent_gui_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ jobs:
cache: "npm"
cache-dependency-path: refact-agent/gui/package-lock.json

# Disable Husky install during npm ci
# Disable Husky install during npm install --prefer-offline
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev
npm pkg delete scripts.prepare
npm ci
npm install --prefer-offline

- run: npm run test
- run: npm run format:check
Expand Down
7 changes: 6 additions & 1 deletion refact-agent/gui/src/app/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,17 @@ function pruneHistory(key: string, item: string) {
}

function removeOldEntry(key: string) {
if (localStorage.getItem(key)) {
if (
typeof localStorage !== "undefined" &&
typeof localStorage.getItem === "function" &&
localStorage.getItem(key)
) {
localStorage.removeItem(key);
}
}

function cleanOldEntries() {
if (typeof localStorage === "undefined") return;
removeOldEntry("tour");
removeOldEntry("tipOfTheDay");
removeOldEntry("chatHistory");
Expand Down
14 changes: 9 additions & 5 deletions refact-agent/gui/src/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
useAppDispatch,
useSendChatRequest,
useAutoSend,
useCapsForToolUse,
} from "../../hooks";
import { type Config } from "../../features/Config/configSlice";
import {
Expand All @@ -25,6 +24,7 @@ import { DropzoneProvider } from "../Dropzone";
import { useCheckpoints } from "../../hooks/useCheckpoints";
import { Checkpoints } from "../../features/Checkpoints";
import { SuggestNewChat } from "../ChatForm/SuggestNewChat";
import { ModelSelector } from "./ModelSelector";

export type ChatProps = {
host: Config["host"];
Expand All @@ -51,7 +51,6 @@ export const Chat: React.FC<ChatProps> = ({
const chatToolUse = useAppSelector(getSelectedToolUse);
const threadNewChatSuggested = useAppSelector(selectThreadNewChatSuggested);
const messages = useAppSelector(selectMessages);
const capsForToolUse = useCapsForToolUse();

const { shouldCheckpointsPopupBeShown } = useCheckpoints();

Expand Down Expand Up @@ -124,13 +123,18 @@ export const Chat: React.FC<ChatProps> = ({
{/* Two flexboxes are left for the future UI element on the right side */}
{messages.length > 0 && (
<Flex align="center" justify="between" width="100%">
<Flex align="center" gap="1">
<Text size="1">model: {capsForToolUse.currentModel} </Text> •{" "}
<Flex align="center" gap="2">
<ModelSelector />
<Text size="1" color="gray">
</Text>
<Text
size="1"
color="gray"
onClick={() => setIsDebugChatHistoryVisible((prev) => !prev)}
style={{ cursor: "pointer" }}
>
mode: {chatToolUse}{" "}
mode: {chatToolUse}
</Text>
</Flex>
{messages.length !== 0 &&
Expand Down
68 changes: 68 additions & 0 deletions refact-agent/gui/src/components/Chat/ModelSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { useMemo } from "react";
import { Select, Text, Flex } from "@radix-ui/themes";
import { useCapsForToolUse } from "../../hooks";

export type ModelSelectorProps = {
disabled?: boolean;
};

export const ModelSelector: React.FC<ModelSelectorProps> = ({ disabled }) => {
const capsForToolUse = useCapsForToolUse();

const modelOptions = useMemo(() => {
return capsForToolUse.usableModelsForPlan.map((model) => ({
value: model.value,
label: model.textValue,
disabled: model.disabled,
}));
}, [capsForToolUse.usableModelsForPlan]);

if (!capsForToolUse.data || modelOptions.length === 0) {
return (
<Text size="1" color="gray">
model: {capsForToolUse.currentModel}
</Text>
);
}

return (
<Flex align="center" gap="1" style={{ height: "20px" }}>
<Text size="1" color="gray" style={{ lineHeight: "20px" }}>
model:
</Text>
<Select.Root
value={capsForToolUse.currentModel}
onValueChange={capsForToolUse.setCapModel}
disabled={disabled}
size="1"
>
<Select.Trigger
variant="ghost"
title={
disabled
? "Cannot change model while streaming"
: "Click to change model"
}
style={{
cursor: disabled ? "not-allowed" : "pointer",
padding: "0 4px",
minHeight: "20px",
height: "20px",
opacity: disabled ? 0.5 : 1,
}}
/>
<Select.Content position="popper">
{modelOptions.map((option) => (
<Select.Item
key={option.value}
value={option.value}
disabled={option.disabled}
>
{option.label}
</Select.Item>
))}
</Select.Content>
</Select.Root>
</Flex>
);
};
2 changes: 2 additions & 0 deletions refact-agent/gui/src/components/Chat/index.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { Chat } from "./Chat";
export type { ChatProps } from "./Chat";
export { ModelSelector } from "./ModelSelector";
export type { ModelSelectorProps } from "./ModelSelector";
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,13 @@ export const GroupTree: React.FC = () => {
onWorkspaceSelection,
availableWorkspaces,
treeHeight,
hasError,
} = useGroupTree();

if (hasError) {
return null;
}

return (
<Flex direction="column" gap="4" mt="4" width="100%">
<Flex direction="column" gap="1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ export function useGroupTree() {
return [];
}, [teamsWorkspaces.data?.query_basic_stuff.workspaces]);

const hasError = teamsWorkspaces.error !== undefined;
const isLoading = teamsWorkspaces.fetching;

return {
// Refs
treeParentRef,
Expand All @@ -222,5 +225,8 @@ export function useGroupTree() {
setCurrentTeamsWorkspace,
setGroupTreeData,
setCurrentSelectedTeamsGroupNode,
// Status
hasError,
isLoading,
};
}
19 changes: 6 additions & 13 deletions refact-agent/gui/src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ import {
import { push } from "../../features/Pages/pagesSlice";
import { restoreChat } from "../../features/Chat/Thread";
import { FeatureMenu } from "../../features/Config/FeatureMenu";
import { GroupTree } from "./GroupTree/";

import { ErrorCallout } from "../Callout";
import { getErrorMessage, clearError } from "../../features/Errors/errorsSlice";
import classNames from "classnames";
import { selectHost } from "../../features/Config/configSlice";
import styles from "./Sidebar.module.css";
import { useActiveTeamsGroup } from "../../hooks/useActiveTeamsGroup";

export type SidebarProps = {
takingNotes: boolean;
Expand All @@ -40,8 +39,6 @@ export const Sidebar: React.FC<SidebarProps> = ({ takingNotes, style }) => {
devModeChecks: { stabilityCheck: "never" },
});

const { groupSelectionEnabled } = useActiveTeamsGroup();

const onDeleteHistoryItem = useCallback(
(id: string) => dispatch(deleteChatById(id)),
[dispatch],
Expand All @@ -64,15 +61,11 @@ export const Sidebar: React.FC<SidebarProps> = ({ takingNotes, style }) => {
</Box>
</Flex>

{!groupSelectionEnabled ? (
<ChatHistory
history={history}
onHistoryItemClick={onHistoryItemClick}
onDeleteHistoryItem={onDeleteHistoryItem}
/>
) : (
<GroupTree />
)}
<ChatHistory
history={history}
onHistoryItemClick={onHistoryItemClick}
onDeleteHistoryItem={onDeleteHistoryItem}
/>
{/* TODO: duplicated */}
{globalError && (
<ErrorCallout
Expand Down
2 changes: 1 addition & 1 deletion refact-agent/gui/src/components/Toolbar/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
{isKnowledgeFeatureAvailable && (
<DropdownMenu.Item
// TODO: get real URL from cloud inference
onSelect={() => openUrl("https://test-teams.smallcloud.ai/")}
onSelect={() => openUrl("https://flexus.team/")}
>
Manage Knowledge
</DropdownMenu.Item>
Expand Down
40 changes: 40 additions & 0 deletions refact-agent/gui/src/components/Tools/Textdoc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import {
TextDocToolCall,
UpdateRegexTextDocToolCall,
UpdateTextDocToolCall,
UpdateTextDocByLinesToolCall,
isCreateTextDocToolCall,
isReplaceTextDocToolCall,
isUpdateRegexTextDocToolCall,
isUpdateTextDocToolCall,
isUpdateTextDocByLinesToolCall,
parseRawTextDocToolCall,
} from "./types";
import { Box, Card, Flex, Button } from "@radix-ui/themes";
Expand Down Expand Up @@ -58,6 +60,10 @@ export const TextDocTool: React.FC<{
return <UpdateRegexTextDoc toolCall={maybeTextDocToolCall} />;
}

if (isUpdateTextDocByLinesToolCall(maybeTextDocToolCall)) {
return <UpdateTextDocByLines toolCall={maybeTextDocToolCall} />;
}

return false;
};

Expand Down Expand Up @@ -94,6 +100,8 @@ const TextDocHeader = forwardRef<HTMLDivElement, TextDocHeaderProps>(
return toolCall.function.arguments.content;
if (isUpdateTextDocToolCall(toolCall))
return toolCall.function.arguments.replacement;
if (isUpdateTextDocByLinesToolCall(toolCall))
return toolCall.function.arguments.content;
return null;
}, [toolCall]);

Expand Down Expand Up @@ -306,6 +314,38 @@ const UpdateTextDoc: React.FC<{
);
};

const UpdateTextDocByLines: React.FC<{
toolCall: UpdateTextDocByLinesToolCall;
}> = ({ toolCall }) => {
const copyToClipBoard = useCopyToClipboard();
const ref = useRef<HTMLDivElement>(null);
const handleClose = useHideScroll(ref);
const handleCopy = useCallback(() => {
copyToClipBoard(toolCall.function.arguments.content);
}, [copyToClipBoard, toolCall.function.arguments.content]);

const className = useMemo(() => {
const extension = getFileExtension(toolCall.function.arguments.path);
return `language-${extension}`;
}, [toolCall.function.arguments.path]);

const lineCount = useMemo(
() => toolCall.function.arguments.content.split("\n").length,
[toolCall.function.arguments.content],
);

return (
<Box className={styles.textdoc}>
<TextDocHeader toolCall={toolCall} ref={ref} />
<Reveal isRevealingCode defaultOpen={lineCount < 9} onClose={handleClose}>
<MarkdownCodeBlock onCopyClick={handleCopy} className={className}>
{toolCall.function.arguments.content}
</MarkdownCodeBlock>
</Reveal>
</Box>
);
};

function getFileExtension(filePath: string): string {
const fileName = filename(filePath);
if (fileName.toLocaleLowerCase().startsWith("dockerfile"))
Expand Down
31 changes: 29 additions & 2 deletions refact-agent/gui/src/components/Tools/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const TEXTDOC_TOOL_NAMES = [
"update_textdoc",
"replace_textdoc",
"update_textdoc_regex",
"update_textdoc_by_lines",
];

type TextDocToolNames = (typeof TEXTDOC_TOOL_NAMES)[number];
Expand Down Expand Up @@ -151,11 +152,36 @@ export const isReplaceTextDocToolCall = (
return true;
};

export interface UpdateTextDocByLinesToolCall extends ParsedRawTextDocToolCall {
function: {
name: string;
arguments: {
path: string;
content: string;
ranges: string;
};
};
}

export const isUpdateTextDocByLinesToolCall = (
toolCall: ParsedRawTextDocToolCall,
): toolCall is UpdateTextDocByLinesToolCall => {
if (toolCall.function.name !== "update_textdoc_by_lines") return false;
if (!("path" in toolCall.function.arguments)) return false;
if (typeof toolCall.function.arguments.path !== "string") return false;
if (!("content" in toolCall.function.arguments)) return false;
if (typeof toolCall.function.arguments.content !== "string") return false;
if (!("ranges" in toolCall.function.arguments)) return false;
if (typeof toolCall.function.arguments.ranges !== "string") return false;
return true;
};

export type TextDocToolCall =
| CreateTextDocToolCall
| UpdateTextDocToolCall
| ReplaceTextDocToolCall
| UpdateRegexTextDocToolCall;
| UpdateRegexTextDocToolCall
| UpdateTextDocByLinesToolCall;

function isTextDocToolCall(
toolCall: ParsedRawTextDocToolCall,
Expand All @@ -164,7 +190,8 @@ function isTextDocToolCall(
if (isUpdateTextDocToolCall(toolCall)) return true;
if (isReplaceTextDocToolCall(toolCall)) return true;
if (isUpdateRegexTextDocToolCall(toolCall)) return true;
return true;
if (isUpdateTextDocByLinesToolCall(toolCall)) return true;
return false;
}

export function parseRawTextDocToolCall(
Expand Down
8 changes: 7 additions & 1 deletion refact-agent/gui/src/features/Chat/Thread/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {
setAreFollowUpsEnabled,
setIsTitleGenerationEnabled,
} from "./actions";
import { formatChatResponse } from "./utils";
import { formatChatResponse, postProcessMessagesAfterStreaming } from "./utils";
import {
ChatMessages,
commandsApi,
Expand Down Expand Up @@ -241,6 +241,9 @@ export const chatReducer = createReducer(initialState, (builder) => {
state.streaming = false;
state.waiting_for_response = false;
state.thread.read = true;
state.thread.messages = postProcessMessagesAfterStreaming(
state.thread.messages,
);
});

builder.addCase(setAutomaticPatch, (state, action) => {
Expand Down Expand Up @@ -323,6 +326,9 @@ export const chatReducer = createReducer(initialState, (builder) => {
new_chat_suggested: { wasSuggested: false },
...mostUptoDateThread,
};
state.thread.messages = postProcessMessagesAfterStreaming(
state.thread.messages,
);
state.thread.tool_use = state.thread.tool_use ?? state.tool_use;
if (action.payload.mode && !isLspChatMode(action.payload.mode)) {
state.thread.mode = "AGENT";
Expand Down
Loading