diff --git a/gui/src/components/indexing/DocsIndexingStatus.tsx b/gui/src/components/indexing/DocsIndexingStatus.tsx
deleted file mode 100644
index 224d96265b..0000000000
--- a/gui/src/components/indexing/DocsIndexingStatus.tsx
+++ /dev/null
@@ -1,218 +0,0 @@
-import { IndexingStatus, SiteIndexingConfig } from "core";
-import { useContext, useMemo, useState } from "react";
-import { IdeMessengerContext } from "../../context/IdeMessenger";
-import {
- ArrowPathIcon,
- ArrowTopRightOnSquareIcon,
- CheckCircleIcon,
- ExclamationTriangleIcon,
- EyeIcon,
- PauseCircleIcon,
- TrashIcon,
-} from "@heroicons/react/24/outline";
-import { useAppDispatch, useAppSelector } from "../../redux/hooks";
-import { updateIndexingStatus } from "../../redux/slices/indexingSlice";
-import { setDialogMessage, setShowDialog } from "../../redux/slices/uiSlice";
-import ConfirmationDialog from "../dialogs/ConfirmationDialog";
-import DocsDetailsDialog from "./DocsDetailsDialog";
-
-interface IndexingStatusViewerProps {
- docConfig: SiteIndexingConfig;
-}
-
-const STATUS_TO_ICON: Record = {
- indexing: ArrowPathIcon,
- paused: PauseCircleIcon,
- complete: CheckCircleIcon,
- aborted: null,
- pending: null,
- failed: ExclamationTriangleIcon, // Since we show an error message below
-};
-
-function DocsIndexingStatus({ docConfig }: IndexingStatusViewerProps) {
- const config = useAppSelector((store) => store.config.config);
- const ideMessenger = useContext(IdeMessengerContext);
- const dispatch = useAppDispatch();
-
- const status = useAppSelector(
- (store) => store.indexing.indexing.statuses[docConfig.startUrl],
- );
-
- const reIndex = () =>
- ideMessenger.post("indexing/reindex", {
- type: "docs",
- id: docConfig.startUrl,
- });
-
- const abort = () => {
- ideMessenger.post("indexing/abort", {
- type: "docs",
- id: docConfig.startUrl,
- });
- // Optimistic abort status
- if (status) {
- dispatch(
- updateIndexingStatus({ ...status, status: "aborted", progress: 0 }),
- );
- }
- };
-
- const [hasDeleted, setHasDeleted] = useState(false); // simple alternative to optimistic redux update
- const onDelete = () => {
- // optimistic update
- dispatch(
- setDialogMessage(
- {
- ideMessenger.post("context/removeDocs", {
- startUrl: docConfig.startUrl,
- });
- setHasDeleted(true);
- }}
- />,
- ),
- );
- dispatch(setShowDialog(true));
- };
-
- const progressPercentage = useMemo(() => {
- if (!status) {
- return 0;
- }
- return Math.min(100, Math.max(0, status.progress * 100)).toFixed(0);
- }, [status?.progress]);
-
- const Icon = STATUS_TO_ICON[status?.status];
- const showProgressPercentage = progressPercentage !== "100";
-
- if (hasDeleted) return null;
-
- return (
-
-
-
{
- if (status?.url) {
- ideMessenger.post("openUrl", status.url);
- }
- }}
- >
- {docConfig.faviconUrl ? (
-
- ) : null}
-
- {docConfig.title ?? docConfig.startUrl}
-
-
-
- {status?.status === "pending" ? (
-
Pending...
- ) : (
-
- {showProgressPercentage && (
- {progressPercentage}%
- )}
- {status?.status !== "indexing" ? (
-
- ) : null}
- {Icon ? (
-
- ) : null}
-
- )}
-
-
-
-
-
-
{},
- pending: () => {},
- }[status?.status]
- }
- >
- {config.disableIndexing
- ? "Indexing disabled"
- : {
- complete: "Click to re-index",
- indexing: "Cancel indexing",
- failed: "Click to retry",
- aborted: "Click to index",
- paused: "",
- pending: "",
- }[status?.status]}
-
-
- {status?.description === "Github rate limit exceeded" ? (
-
- ideMessenger.post(
- "openUrl",
- "https://docs.continue.dev/customize/deep-dives/docs#github",
- )
- }
- >
- {status.description}
-
- ) : (
-
- {status?.description}
-
- )}
- {status?.status === "complete" ? (
- {
- dispatch(setShowDialog(true));
- dispatch(
- setDialogMessage(
- ,
- ),
- );
- }}
- >
- Add Docs
-
- ) : null}
-
-
-
- );
-}
-
-export default DocsIndexingStatus;
diff --git a/gui/src/components/indexing/DocsIndexingStatuses.tsx b/gui/src/components/indexing/DocsIndexingStatuses.tsx
deleted file mode 100644
index f1e261dfb3..0000000000
--- a/gui/src/components/indexing/DocsIndexingStatuses.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import { useDispatch } from "react-redux";
-import { SecondaryButton } from "..";
-import { setDialogMessage, setShowDialog } from "../../redux/slices/uiSlice";
-import AddDocsDialog from "../dialogs/AddDocsDialog";
-import DocsIndexingStatus from "./DocsIndexingStatus";
-import { useAppSelector } from "../../redux/hooks";
-import { useContext, useMemo } from "react";
-import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
-import { IdeMessengerContext } from "../../context/IdeMessenger";
-import { IndexingStatus } from "core";
-
-function DocsIndexingStatuses() {
- const dispatch = useDispatch();
- const config = useAppSelector((store) => store.config.config);
- const ideMessenger = useContext(IdeMessengerContext);
- const indexingStatuses = useAppSelector(
- (store) => store.indexing.indexing.statuses,
- );
-
- const hasDocsProvider = useMemo(() => {
- return !!config.contextProviders?.some(
- (provider) => provider.title === "docs",
- );
- }, [config]);
-
- // TODO - this might significantly impact performance during indexing
- const sortedConfigDocs = useMemo(() => {
- const sorter = (status: IndexingStatus["status"]) => {
- // TODO - further sorting?
- if (status === "indexing" || status === "paused") return 0;
- if (status === "failed") return 1;
- if (status === "aborted" || status === "pending") return 2;
- return 3;
- };
-
- const docs = [...(config.docs ?? [])];
- docs.sort((a, b) =>
- sorter(indexingStatuses[b.startUrl]?.status ?? "pending") >
- sorter(indexingStatuses[a.startUrl]?.status ?? "pending")
- ? -1
- : 1,
- );
- return docs;
- }, [config, indexingStatuses]);
-
- return (
-
-
-
@docs indexes
- {sortedConfigDocs.length ? (
-
{
- dispatch(setShowDialog(true));
- dispatch(setDialogMessage( ));
- }}
- >
- Add
-
- ) : null}
-
-
- {hasDocsProvider ? (
- sortedConfigDocs.length ? (
- "Manage your documentation sources"
- ) : (
- "No docs yet"
- )
- ) : (
-
-
-
-
-
-
- @docs is not in your config
-
-
-
{
- ideMessenger.post("config/addContextProvider", {
- name: "docs",
- params: {},
- });
- }}
- >
- Add @docs to my config
-
-
- )}
-
-
-
- {sortedConfigDocs.length === 0 && (
-
{
- dispatch(setShowDialog(true));
- dispatch(setDialogMessage( ));
- }}
- >
- Add Docs
-
- )}
-
- {sortedConfigDocs.map((doc) => {
- return
;
- })}
-
-
- );
-}
-
-export default DocsIndexingStatuses;
diff --git a/gui/src/components/mainInput/MentionList.tsx b/gui/src/components/mainInput/AtMentionDropdown/index.tsx
similarity index 95%
rename from gui/src/components/mainInput/MentionList.tsx
rename to gui/src/components/mainInput/AtMentionDropdown/index.tsx
index 66c2da16e6..176120153f 100644
--- a/gui/src/components/mainInput/MentionList.tsx
+++ b/gui/src/components/mainInput/AtMentionDropdown/index.tsx
@@ -22,15 +22,15 @@ import {
vscListActiveBackground,
vscListActiveForeground,
vscQuickInputBackground,
-} from "..";
-import { IdeMessengerContext } from "../../context/IdeMessenger";
-import { setDialogMessage, setShowDialog } from "../../redux/slices/uiSlice";
-import FileIcon from "../FileIcon";
-import SafeImg from "../SafeImg";
-import AddDocsDialog from "../dialogs/AddDocsDialog";
-import HeaderButtonWithToolTip from "../gui/HeaderButtonWithToolTip";
-import { NAMED_ICONS } from "./icons";
-import { ComboBoxItem, ComboBoxItemType } from "./types";
+} from "../..";
+import { IdeMessengerContext } from "../../../context/IdeMessenger";
+import { setDialogMessage, setShowDialog } from "../../../redux/slices/uiSlice";
+import FileIcon from "../../FileIcon";
+import SafeImg from "../../SafeImg";
+import AddDocsDialog from "../../dialogs/AddDocsDialog";
+import HeaderButtonWithToolTip from "../../gui/HeaderButtonWithToolTip";
+import { NAMED_ICONS } from "../icons";
+import { ComboBoxItem, ComboBoxItemType } from "../types";
export function getIconFromDropdownItem(
id: string | undefined,
@@ -129,7 +129,7 @@ const QueryInput = styled.textarea`
resize: none;
`;
-interface MentionListProps {
+interface AtMentionDropdownProps {
items: ComboBoxItem[];
command: (item: any) => void;
@@ -138,7 +138,7 @@ interface MentionListProps {
onClose: () => void;
}
-const MentionList = forwardRef((props: MentionListProps, ref) => {
+const AtMentionDropdown = forwardRef((props: AtMentionDropdownProps, ref) => {
const dispatch = useDispatch();
const ideMessenger = useContext(IdeMessengerContext);
@@ -460,4 +460,4 @@ const MentionList = forwardRef((props: MentionListProps, ref) => {
);
});
-export default MentionList;
+export default AtMentionDropdown;
diff --git a/gui/src/components/mainInput/ContinueButton.tsx b/gui/src/components/mainInput/ContinueButton.tsx
deleted file mode 100644
index 23776bbcf8..0000000000
--- a/gui/src/components/mainInput/ContinueButton.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import { PlayIcon, StopIcon } from "@heroicons/react/24/outline";
-import styled from "styled-components";
-import { Button } from "..";
-import { getFontSize } from "../../util";
-
-const StyledButton = styled(Button)<{
- color?: string | null;
- isDisabled: boolean;
- showStop: boolean;
-}>`
- margin: auto;
- margin-top: 8px;
- margin-bottom: 16px;
- display: grid;
- width: 130px;
- grid-template-columns: 22px 1fr;
- align-items: center;
- background-color: ${(props) =>
- `${props.color || "#be1b55"}${props.showStop ? "33" : ""}`};
-
- opacity: ${(props) => (props.isDisabled ? 0.5 : 1.0)};
-
- border: 1px solid
- ${(props) => (props.showStop ? props.color || "#be1b55" : "transparent")};
-
- cursor: ${(props) => (props.isDisabled ? "default" : "pointer")};
-
- &:hover:enabled {
- background-color: ${(props) =>
- `${props.color || "#be1b55"}${props.showStop ? "33" : ""}`};
- ${(props) =>
- props.isDisabled
- ? "cursor: default;"
- : `
- opacity: 0.7;
- `}
- }
-`;
-
-function ContinueButton(props: {
- onClick?: () => void;
- hidden?: boolean;
- disabled: boolean;
- showStop: boolean;
-}) {
- return (
-
- {props.showStop ? (
- <>
-
- STOP
- >
- ) : (
- <>
- {window.vscMediaUrl ? (
-
- ) : (
-
- )}
- CONTINUE
- >
- )}
-
- );
-}
-
-export default ContinueButton;
diff --git a/gui/src/components/mainInput/ContinueInputBox.tsx b/gui/src/components/mainInput/ContinueInputBox.tsx
index 69264eb35f..2b91b707b5 100644
--- a/gui/src/components/mainInput/ContinueInputBox.tsx
+++ b/gui/src/components/mainInput/ContinueInputBox.tsx
@@ -1,14 +1,14 @@
import { Editor, JSONContent } from "@tiptap/react";
import { ContextItemWithId, InputModifiers } from "core";
-import { useDispatch } from "react-redux";
+import { useMemo, useState } from "react";
import styled, { keyframes } from "styled-components";
import { defaultBorderRadius, vscBackground } from "..";
-import { selectSlashCommandComboBoxInputs } from "../../redux/selectors";
-import ContextItemsPeek from "./ContextItemsPeek";
-import TipTapEditor from "./TipTapEditor";
import { useAppSelector } from "../../redux/hooks";
+import { selectSlashCommandComboBoxInputs } from "../../redux/selectors";
+import ContextItemsPeek from "./belowMainInput/ContextItemsPeek";
import { ToolbarOptions } from "./InputToolbar";
-import { useMemo } from "react";
+import { Lump } from "./Lump";
+import TipTapEditor from "./tiptap/TipTapEditor";
interface ContinueInputBoxProps {
isEditMode?: boolean;
@@ -113,9 +113,12 @@ function ContinueInputBox(props: ContinueInputBoxProps) {
}
: {};
+ const [lumpOpen, setLumpOpen] = useState(true);
+
return (
+ {props.isMainInput &&
}
diff --git a/gui/src/components/mainInput/InputToolbar.tsx b/gui/src/components/mainInput/InputToolbar.tsx
index a1f9da2820..f3a8b975f2 100644
--- a/gui/src/components/mainInput/InputToolbar.tsx
+++ b/gui/src/components/mainInput/InputToolbar.tsx
@@ -21,6 +21,7 @@ import {
import { exitEditMode } from "../../redux/thunks";
import { loadLastSession } from "../../redux/thunks/session";
import {
+ fontSize,
getAltKeyLabel,
getFontSize,
getMetaKeyLabel,
@@ -28,8 +29,8 @@ import {
} from "../../util";
import { ToolTip } from "../gui/Tooltip";
import ModelSelect from "../modelSelection/ModelSelect";
-import HoverItem from "./InputToolbar/HoverItem";
-import ToggleToolsButton from "./InputToolbar/ToggleToolsButton";
+import ModeSelect from "../modelSelection/ModeSelect";
+import HoverItem from "./InputToolbar/bottom/HoverItem";
const StyledDiv = styled.div<{ isHidden?: boolean }>`
padding-top: 4px;
@@ -82,6 +83,8 @@ interface InputToolbarProps {
toolbarOptions?: ToolbarOptions;
disabled?: boolean;
isMainInput?: boolean;
+ lumpOpen: boolean;
+ setLumpOpen: (open: boolean) => void;
}
function InputToolbar(props: InputToolbarProps) {
@@ -112,6 +115,7 @@ function InputToolbar(props: InputToolbarProps) {
className="find-widget-skip flex"
>
+
{props.toolbarOptions?.hideImageUpload ||
@@ -131,7 +135,7 @@ function InputToolbar(props: InputToolbarProps) {
/>
{
fileInputRef.current?.click();
@@ -147,7 +151,7 @@ function InputToolbar(props: InputToolbarProps) {
@@ -155,12 +159,15 @@ function InputToolbar(props: InputToolbarProps) {
)}
-
-
-
+
{!props.toolbarOptions?.hideUseCodebase && !isInEditMode && (
state.ui.useTools);
- const isInEditMode = useAppSelector(selectIsInEditMode);
-
- const ToolsIcon = useTools
- ? WrenchScrewdriverIconSolid
- : WrenchScrewdriverIconOutline;
-
- function showTools() {
- dispatch(setDialogMessage(
));
- dispatch(setShowDialog(true));
- }
-
- const isDisabled = props.disabled || isInEditMode;
-
- return (
-
!isDisabled && dispatch(toggleUseTools())}>
-
-
!isDisabled && setIsHovered(true)}
- onMouseLeave={() => !isDisabled && setIsHovered(false)}
- />
- {isDisabled && (
-
- {isInEditMode
- ? "Tool use not supported in edit mode"
- : "This model does not support tool use"}
-
- )}
- {!useTools && !isDisabled && (
-
- Enable tool usage
-
- )}
-
- {useTools && !isDisabled && (
- <>
- Tools
- {
- e.stopPropagation();
- showTools();
- }}
- className="text-lightgray flex cursor-pointer items-center"
- aria-disabled={isDisabled}
- >
-
-
- Tool policies
- >
- )}
-
-
- );
-}
diff --git a/gui/src/components/mainInput/InputToolbar/ToolPermissionsDialog.tsx b/gui/src/components/mainInput/InputToolbar/ToolPermissionsDialog.tsx
deleted file mode 100644
index 731e27a348..0000000000
--- a/gui/src/components/mainInput/InputToolbar/ToolPermissionsDialog.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import { Tool } from "core";
-import { useMemo } from "react";
-import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
-import { toggleToolGroupSetting } from "../../../redux/slices/uiSlice";
-import ToggleSwitch from "../../gui/Switch";
-import InfoHover from "../../InfoHover";
-import ToolDropdownItem from "./ToolDropdownItem";
-
-const ToolPermissionsDialog = () => {
- const availableTools = useAppSelector((state) => state.config.config.tools);
- const toolGroupSettings = useAppSelector(
- (store) => store.ui.toolGroupSettings,
- );
- const dispatch = useAppDispatch();
-
- const toolsByGroup = useMemo(() => {
- const byGroup: Record
= {};
- for (const tool of availableTools) {
- if (!byGroup[tool.group]) {
- byGroup[tool.group] = [];
- }
- byGroup[tool.group].push(tool);
- }
- return Object.entries(byGroup);
- }, [availableTools]);
-
- // Detect duplicate tool names
- const duplicateDetection = useMemo(() => {
- const counts: Record = {};
- availableTools.forEach((tool) => {
- if (counts[tool.function.name]) {
- counts[tool.function.name] = counts[tool.function.name] + 1;
- } else {
- counts[tool.function.name] = 1;
- }
- });
- return Object.fromEntries(
- Object.entries(counts).map(([k, v]) => [k, v > 1]),
- );
- }, [availableTools]);
-
- return (
-
-
-
Tool policies
-
-
- Automatic: Can be used
- without asking
-
-
- Allowed: Will ask
- before using
-
-
- Disabled: Cannot be used
-
-
- }
- />
-
-
- {toolsByGroup.map(([groupName, tools]) => (
-
-
-
{groupName}
- dispatch(toggleToolGroupSetting(groupName))}
- text=""
- size={12}
- />
-
-
- {tools.map((tool) => (
-
- ))}
- {toolGroupSettings[groupName] === "exclude" && (
-
- )}
-
-
- ))}
-
-
- );
-};
-
-export default ToolPermissionsDialog;
diff --git a/gui/src/components/mainInput/InputToolbar/HoverItem.tsx b/gui/src/components/mainInput/InputToolbar/bottom/HoverItem.tsx
similarity index 65%
rename from gui/src/components/mainInput/InputToolbar/HoverItem.tsx
rename to gui/src/components/mainInput/InputToolbar/bottom/HoverItem.tsx
index e668a69f2a..4a8aefcdc6 100644
--- a/gui/src/components/mainInput/InputToolbar/HoverItem.tsx
+++ b/gui/src/components/mainInput/InputToolbar/bottom/HoverItem.tsx
@@ -1,7 +1,7 @@
import styled from "styled-components";
-const HoverItem = styled.span<{ isActive?: boolean }>`
- padding: 0 4px;
+const HoverItem = styled.span<{ isActive?: boolean; px?: number }>`
+ padding: 0 ${(props) => props.px ?? 4}px;
padding-top: 2px;
padding-bottom: 2px;
cursor: pointer;
diff --git a/gui/src/components/mainInput/InputToolbar/PopoverTransition.tsx b/gui/src/components/mainInput/InputToolbar/bottom/PopoverTransition.tsx
similarity index 100%
rename from gui/src/components/mainInput/InputToolbar/PopoverTransition.tsx
rename to gui/src/components/mainInput/InputToolbar/bottom/PopoverTransition.tsx
diff --git a/gui/src/components/mainInput/InputToolbar/ToolDropdownItem.tsx b/gui/src/components/mainInput/InputToolbar/bottom/ToolDropdownItem.tsx
similarity index 88%
rename from gui/src/components/mainInput/InputToolbar/ToolDropdownItem.tsx
rename to gui/src/components/mainInput/InputToolbar/bottom/ToolDropdownItem.tsx
index 8926221247..ca423764b8 100644
--- a/gui/src/components/mainInput/InputToolbar/ToolDropdownItem.tsx
+++ b/gui/src/components/mainInput/InputToolbar/bottom/ToolDropdownItem.tsx
@@ -2,9 +2,10 @@ import { InformationCircleIcon } from "@heroicons/react/24/outline";
import { Tool } from "core";
import { useEffect } from "react";
import { useDispatch } from "react-redux";
-import { useAppSelector } from "../../../redux/hooks";
-import { addTool, toggleToolSetting } from "../../../redux/slices/uiSlice";
-import { ToolTip } from "../../gui/Tooltip";
+import { useAppSelector } from "../../../../redux/hooks";
+import { addTool, toggleToolSetting } from "../../../../redux/slices/uiSlice";
+import { fontSize } from "../../../../util";
+import { ToolTip } from "../../../gui/Tooltip";
interface ToolDropdownItemProps {
tool: Tool;
@@ -30,7 +31,10 @@ function ToolDropdownItem(props: ToolDropdownItemProps) {
return (
{
dispatch(toggleToolSetting(props.tool.function.name));
e.stopPropagation();
diff --git a/gui/src/components/mainInput/InputToolbar/bottom/ToolPermissionsDialog.tsx b/gui/src/components/mainInput/InputToolbar/bottom/ToolPermissionsDialog.tsx
new file mode 100644
index 0000000000..5d3c155c2f
--- /dev/null
+++ b/gui/src/components/mainInput/InputToolbar/bottom/ToolPermissionsDialog.tsx
@@ -0,0 +1,79 @@
+import { Tool } from "core";
+import { useMemo } from "react";
+import { useAppDispatch, useAppSelector } from "../../../../redux/hooks";
+import { toggleToolGroupSetting } from "../../../../redux/slices/uiSlice";
+import { fontSize } from "../../../../util";
+import ToggleSwitch from "../../../gui/Switch";
+import ToolDropdownItem from "./ToolDropdownItem";
+
+export const ToolPermissionsDialog = () => {
+ const availableTools = useAppSelector((state) => state.config.config.tools);
+ const toolGroupSettings = useAppSelector(
+ (store) => store.ui.toolGroupSettings,
+ );
+ const dispatch = useAppDispatch();
+
+ const toolsByGroup = useMemo(() => {
+ const byGroup: Record
= {};
+ for (const tool of availableTools) {
+ if (!byGroup[tool.group]) {
+ byGroup[tool.group] = [];
+ }
+ byGroup[tool.group].push(tool);
+ }
+ return Object.entries(byGroup);
+ }, [availableTools]);
+
+ // Detect duplicate tool names
+ const duplicateDetection = useMemo(() => {
+ const counts: Record = {};
+ availableTools.forEach((tool) => {
+ if (counts[tool.function.name]) {
+ counts[tool.function.name] = counts[tool.function.name] + 1;
+ } else {
+ counts[tool.function.name] = 1;
+ }
+ });
+ return Object.fromEntries(
+ Object.entries(counts).map(([k, v]) => [k, v > 1]),
+ );
+ }, [availableTools]);
+
+ return (
+ <>
+ {toolsByGroup.map(([groupName, tools]) => (
+
+
+
+ {groupName}
+
+ dispatch(toggleToolGroupSetting(groupName))}
+ text=""
+ size={10}
+ />
+
+
+ {tools.map((tool) => (
+
+ ))}
+ {toolGroupSettings[groupName] === "exclude" && (
+
+ )}
+
+
+ ))}
+ >
+ );
+};
diff --git a/gui/src/components/mainInput/InputToolbar/top/TopInputToolbar.tsx b/gui/src/components/mainInput/InputToolbar/top/TopInputToolbar.tsx
new file mode 100644
index 0000000000..f82ea9860e
--- /dev/null
+++ b/gui/src/components/mainInput/InputToolbar/top/TopInputToolbar.tsx
@@ -0,0 +1,22 @@
+import { EllipsisHorizontalIcon } from "@heroicons/react/24/outline";
+import { ToolbarOptions } from "../../InputToolbar";
+import HoverItem from "../bottom/HoverItem";
+
+interface TopInputToolbarProps {
+ toolbarOptions?: ToolbarOptions;
+ onAddContextItem?: () => void;
+ lumpOpen: boolean;
+ setLumpOpen: (open: boolean) => void;
+}
+export function TopInputToolbar(props: TopInputToolbarProps) {
+ return (
+
+ props.setLumpOpen(!props.lumpOpen)}
+ className="ml-auto"
+ >
+
+
+
+ );
+}
diff --git a/gui/src/components/mainInput/Lump/BlockSettingsTopToolbar.tsx b/gui/src/components/mainInput/Lump/BlockSettingsTopToolbar.tsx
new file mode 100644
index 0000000000..f6abe9e858
--- /dev/null
+++ b/gui/src/components/mainInput/Lump/BlockSettingsTopToolbar.tsx
@@ -0,0 +1,135 @@
+import {
+ BookOpenIcon,
+ ChatBubbleLeftIcon,
+ ChevronLeftIcon,
+ CubeIcon,
+ EllipsisHorizontalIcon,
+ PencilIcon,
+ Squares2X2Icon,
+ WrenchScrewdriverIcon,
+} from "@heroicons/react/24/outline";
+import { vscBadgeBackground, vscBadgeForeground } from "../..";
+import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
+import { toggleBlockSettingsToolbar } from "../../../redux/slices/uiSlice";
+import { fontSize } from "../../../util";
+import AssistantSelect from "../../modelSelection/platform/AssistantSelect";
+import HoverItem from "../InputToolbar/bottom/HoverItem";
+
+interface BlockSettingsToolbarIcon {
+ tooltip: string;
+ icon: React.ComponentType;
+ itemCount?: number;
+ onClick: () => void;
+ isSelected?: boolean;
+}
+
+interface Section {
+ id: string;
+ tooltip: string;
+ icon: React.ComponentType;
+}
+
+const sections: Section[] = [
+ { id: "models", tooltip: "Models", icon: CubeIcon },
+ { id: "rules", tooltip: "Rules", icon: PencilIcon },
+ { id: "docs", tooltip: "Docs", icon: BookOpenIcon },
+ { id: "prompts", tooltip: "Prompts", icon: ChatBubbleLeftIcon },
+ { id: "tools", tooltip: "Tools", icon: WrenchScrewdriverIcon },
+ { id: "mcp", tooltip: "MCP", icon: Squares2X2Icon },
+];
+
+function BlockSettingsToolbarIcon(props: BlockSettingsToolbarIcon) {
+ return (
+
+ {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ props.onClick();
+ }
+ }}
+ style={{
+ backgroundColor: props.isSelected ? vscBadgeBackground : undefined,
+ }}
+ className={`relative flex select-none items-center rounded-full px-1 transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50`}
+ >
+
+
+
+ {props.tooltip}
+
+
+
+
+ );
+}
+
+interface BlockSettingsTopToolbarProps {
+ selectedSection: string | null;
+ setSelectedSection: (value: string | null) => void;
+}
+
+export function BlockSettingsTopToolbar(props: BlockSettingsTopToolbarProps) {
+ const isExpanded = useAppSelector(
+ (state) => state.ui.isBlockSettingsToolbarExpanded,
+ );
+ const dispatch = useAppDispatch();
+ const handleEllipsisClick = () => {
+ if (isExpanded) {
+ props.setSelectedSection(null);
+ }
+ dispatch(toggleBlockSettingsToolbar());
+ };
+
+ return (
+
+
+
+
+
+ {sections.map((section) => (
+
+ props.setSelectedSection(
+ props.selectedSection === section.id ? null : section.id,
+ )
+ }
+ />
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/gui/src/components/mainInput/Lump/LumpToolbar.tsx b/gui/src/components/mainInput/Lump/LumpToolbar.tsx
new file mode 100644
index 0000000000..cca7885d39
--- /dev/null
+++ b/gui/src/components/mainInput/Lump/LumpToolbar.tsx
@@ -0,0 +1,73 @@
+import { useContext } from "react";
+import styled from "styled-components";
+import { IdeMessengerContext } from "../../../context/IdeMessenger";
+import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
+import {
+ clearLastEmptyResponse,
+ setInactive,
+} from "../../../redux/slices/sessionSlice";
+import { getFontSize, getMetaKeyLabel } from "../../../util";
+import { BlockSettingsTopToolbar } from "./BlockSettingsTopToolbar";
+
+const Container = styled.div`
+ display: flex;
+ justify-content: flex-end;
+ width: 100%;
+`;
+
+const StopButton = styled.div`
+ font-size: ${getFontSize() - 3}px;
+ padding: 2px;
+ padding-right: 4px;
+ cursor: pointer;
+`;
+
+interface TopToolbarProps {
+ selectedSection: string | null;
+ setSelectedSection: (value: string | null) => void;
+}
+
+export function LumpToolbar(props: TopToolbarProps) {
+ const dispatch = useAppDispatch();
+ const ideMessenger = useContext(IdeMessengerContext);
+ const ttsActive = useAppSelector((state) => state.ui.ttsActive);
+ const isStreaming = useAppSelector((state) => state.session.isStreaming);
+
+ if (ttsActive) {
+ return (
+
+ {
+ ideMessenger.post("tts/kill", undefined);
+ }}
+ >
+ ■ Stop TTS
+
+
+ );
+ }
+
+ if (isStreaming) {
+ return (
+
+ {
+ dispatch(setInactive());
+ dispatch(clearLastEmptyResponse());
+ }}
+ >
+ {getMetaKeyLabel()} ⌫ Cancel
+
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/gui/src/components/mainInput/Lump/index.tsx b/gui/src/components/mainInput/Lump/index.tsx
new file mode 100644
index 0000000000..f46f432d09
--- /dev/null
+++ b/gui/src/components/mainInput/Lump/index.tsx
@@ -0,0 +1,75 @@
+import { useEffect, useState } from "react";
+import styled from "styled-components";
+import {
+ defaultBorderRadius,
+ vscCommandCenterInactiveBorder,
+ vscInputBackground,
+} from "../..";
+import { LumpToolbar } from "./LumpToolbar";
+import { SelectedSection } from "./sections/SelectedSection";
+
+interface LumpProps {
+ open: boolean;
+ setOpen: (open: boolean) => void;
+}
+
+const LumpDiv = styled.div<{ open: boolean }>`
+ background-color: ${vscInputBackground};
+ margin-left: 4px;
+ margin-right: 4px;
+ border-radius: ${defaultBorderRadius} ${defaultBorderRadius} 0 0;
+ border-top: 1px solid ${vscCommandCenterInactiveBorder};
+ border-left: 1px solid ${vscCommandCenterInactiveBorder};
+ border-right: 1px solid ${vscCommandCenterInactiveBorder};
+`;
+
+const ContentDiv = styled.div<{ hasSection: boolean; isVisible: boolean }>`
+ transition:
+ max-height 0.3s ease-in-out,
+ margin 0.3s ease-in-out,
+ opacity 0.2s ease-in-out;
+ max-height: ${(props) => (props.hasSection ? "200px" : "0px")};
+ margin: ${(props) => (props.hasSection ? "4px 0" : "0")};
+ opacity: ${(props) => (props.isVisible ? "1" : "0")};
+ overflow-y: auto;
+`;
+
+export function Lump(props: LumpProps) {
+ const { open, setOpen } = props;
+ const [selectedSection, setSelectedSection] = useState(null);
+ const [displayedSection, setDisplayedSection] = useState(null);
+ const [isVisible, setIsVisible] = useState(false);
+
+ useEffect(() => {
+ if (selectedSection) {
+ setDisplayedSection(selectedSection);
+ setIsVisible(true);
+ } else {
+ setIsVisible(false);
+ // Delay clearing the displayed section until after the fade-out
+ const timeout = setTimeout(() => {
+ setDisplayedSection(null);
+ }, 300); // Match the transition duration
+ return () => clearTimeout(timeout);
+ }
+ }, [selectedSection]);
+
+ if (!open) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/gui/src/components/mainInput/Lump/sections/AddBlockButton.tsx b/gui/src/components/mainInput/Lump/sections/AddBlockButton.tsx
new file mode 100644
index 0000000000..b409d160fd
--- /dev/null
+++ b/gui/src/components/mainInput/Lump/sections/AddBlockButton.tsx
@@ -0,0 +1,54 @@
+import { PlusIcon } from "@heroicons/react/24/outline";
+import { useContext } from "react";
+import { useAuth } from "../../../../context/Auth";
+import { IdeMessengerContext } from "../../../../context/IdeMessenger";
+import { useAppDispatch } from "../../../../redux/hooks";
+import {
+ setDialogMessage,
+ setShowDialog,
+} from "../../../../redux/slices/uiSlice";
+import { fontSize } from "../../../../util";
+import AddDocsDialog from "../../../dialogs/AddDocsDialog";
+
+export function AddBlockButton(props: { blockType: string }) {
+ const { selectedProfile } = useAuth();
+ const ideMessenger = useContext(IdeMessengerContext);
+ const dispatch = useAppDispatch();
+
+ const handleClick = () => {
+ if (selectedProfile?.profileType === "local") {
+ switch (props.blockType) {
+ case "docs":
+ dispatch(setShowDialog(true));
+ dispatch(setDialogMessage( ));
+ break;
+ default:
+ ideMessenger.request("config/openProfile", {
+ profileId: selectedProfile.id,
+ });
+ }
+ } else {
+ ideMessenger.request("controlPlane/openUrl", {
+ path: `new?type=block&blockType=${props.blockType}`,
+ orgSlug: undefined,
+ });
+ }
+ };
+
+ return (
+ {
+ e.preventDefault();
+ handleClick();
+ }}
+ >
+
+
+ );
+}
diff --git a/gui/src/components/mainInput/Lump/sections/ContextSection.tsx b/gui/src/components/mainInput/Lump/sections/ContextSection.tsx
new file mode 100644
index 0000000000..7524fd08d8
--- /dev/null
+++ b/gui/src/components/mainInput/Lump/sections/ContextSection.tsx
@@ -0,0 +1,3 @@
+export function ContextSection() {
+ return Context content
;
+}
diff --git a/gui/src/components/mainInput/Lump/sections/MCPSection.tsx b/gui/src/components/mainInput/Lump/sections/MCPSection.tsx
new file mode 100644
index 0000000000..b365dca34f
--- /dev/null
+++ b/gui/src/components/mainInput/Lump/sections/MCPSection.tsx
@@ -0,0 +1,5 @@
+import MCPServersPreview from "../../../../pages/config/MCPServersPreview";
+
+export function MCPSection() {
+ return ;
+}
diff --git a/gui/src/components/mainInput/Lump/sections/ModelsSection.tsx b/gui/src/components/mainInput/Lump/sections/ModelsSection.tsx
new file mode 100644
index 0000000000..cd16e84534
--- /dev/null
+++ b/gui/src/components/mainInput/Lump/sections/ModelsSection.tsx
@@ -0,0 +1,110 @@
+import { ModelRole } from "@continuedev/config-yaml";
+import { ModelDescription } from "core";
+import { useContext } from "react";
+import { useAuth } from "../../../../context/Auth";
+import { IdeMessengerContext } from "../../../../context/IdeMessenger";
+import ModelRoleSelector from "../../../../pages/config/ModelRoleSelector";
+import { useAppDispatch, useAppSelector } from "../../../../redux/hooks";
+import {
+ selectDefaultModel,
+ setDefaultModel,
+ updateConfig,
+} from "../../../../redux/slices/configSlice";
+import { isJetBrains } from "../../../../util";
+
+export function ModelsSection() {
+ const { selectedProfile } = useAuth();
+ const dispatch = useAppDispatch();
+ const ideMessenger = useContext(IdeMessengerContext);
+
+ const config = useAppSelector((state) => state.config.config);
+ const jetbrains = isJetBrains();
+ const selectedChatModel = useAppSelector(selectDefaultModel);
+
+ function handleRoleUpdate(role: ModelRole, model: ModelDescription | null) {
+ if (!selectedProfile) {
+ return;
+ }
+ // Optimistic update
+ dispatch(
+ updateConfig({
+ ...config,
+ selectedModelByRole: {
+ ...config.selectedModelByRole,
+ [role]: model,
+ },
+ }),
+ );
+ ideMessenger.post("config/updateSelectedModel", {
+ profileId: selectedProfile.id,
+ role,
+ title: model?.title ?? null,
+ });
+ }
+
+ // TODO use handleRoleUpdate for chat
+ function handleChatModelSelection(model: ModelDescription | null) {
+ if (!model) {
+ return;
+ }
+ dispatch(setDefaultModel({ title: model.title }));
+ }
+
+ return (
+
+ handleChatModelSelection(model)}
+ />
+ handleRoleUpdate("autocomplete", model)}
+ />
+ {/* Jetbrains has a model selector inline */}
+ {!jetbrains && (
+ handleRoleUpdate("edit", model)}
+ />
+ )}
+ handleRoleUpdate("apply", model)}
+ />
+ handleRoleUpdate("embed", model)}
+ />
+ handleRoleUpdate("rerank", model)}
+ />
+
+ );
+}
diff --git a/gui/src/components/mainInput/Lump/sections/PromptsSection.tsx b/gui/src/components/mainInput/Lump/sections/PromptsSection.tsx
new file mode 100644
index 0000000000..38ce052091
--- /dev/null
+++ b/gui/src/components/mainInput/Lump/sections/PromptsSection.tsx
@@ -0,0 +1,80 @@
+import {
+ BookmarkIcon as BookmarkOutline,
+ PencilIcon,
+} from "@heroicons/react/24/outline";
+import { BookmarkIcon as BookmarkSolid } from "@heroicons/react/24/solid";
+import { fontSize } from "../../../../util";
+import { useBookmarkedSlashCommands } from "../../../ConversationStarters/useBookmarkedSlashCommands";
+import { AddBlockButton } from "./AddBlockButton";
+
+interface PromptRowProps {
+ command: string;
+ description: string;
+ isBookmarked: boolean;
+ setIsBookmarked: (isBookmarked: boolean) => void;
+ onEdit?: () => void;
+}
+
+function PromptRow({
+ command,
+ description,
+ isBookmarked,
+ setIsBookmarked,
+ onEdit,
+}: PromptRowProps) {
+ return (
+
+
+ {command}
+ {description}
+
+
+
+
setIsBookmarked(!isBookmarked)}
+ className="cursor-pointer pt-0.5 text-gray-400"
+ >
+ {isBookmarked ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
+
+export function PromptsSection() {
+ const { cmdsSortedByBookmark, bookmarkStatuses, toggleBookmark } =
+ useBookmarkedSlashCommands();
+
+ const handleEdit = (prompt: any) => {
+ // Handle edit action here
+ console.log("Editing prompt:", prompt);
+ };
+
+ return (
+
+ {cmdsSortedByBookmark?.map((prompt, i) => (
+
toggleBookmark(prompt)}
+ onEdit={() => handleEdit(prompt)}
+ />
+ ))}
+
+
+ );
+}
diff --git a/gui/src/pages/More/RulesPreview/index.tsx b/gui/src/components/mainInput/Lump/sections/RulesSection.tsx
similarity index 72%
rename from gui/src/pages/More/RulesPreview/index.tsx
rename to gui/src/components/mainInput/Lump/sections/RulesSection.tsx
index 6a5200a8a7..200472b22b 100644
--- a/gui/src/pages/More/RulesPreview/index.tsx
+++ b/gui/src/components/mainInput/Lump/sections/RulesSection.tsx
@@ -1,11 +1,14 @@
import { parseConfigYaml } from "@continuedev/config-yaml";
-import { PencilSquareIcon } from "@heroicons/react/24/outline";
+import { PencilIcon } from "@heroicons/react/24/outline";
import { useContext, useMemo } from "react";
import { useSelector } from "react-redux";
-import HeaderButtonWithToolTip from "../../../components/gui/HeaderButtonWithToolTip";
-import { useAuth } from "../../../context/Auth";
-import { IdeMessengerContext } from "../../../context/IdeMessenger";
-import { RootState } from "../../../redux/store";
+import { defaultBorderRadius, vscCommandCenterActiveBorder } from "../../..";
+import { useAuth } from "../../../../context/Auth";
+import { IdeMessengerContext } from "../../../../context/IdeMessenger";
+import { RootState } from "../../../../redux/store";
+import { fontSize } from "../../../../util";
+import HeaderButtonWithToolTip from "../../../gui/HeaderButtonWithToolTip";
+import { AddBlockButton } from "./AddBlockButton";
interface RuleCardProps {
index: number;
@@ -23,23 +26,42 @@ const RuleCard: React.FC = ({ index, rule, onClick, title }) => {
};
return (
-
+
-
{title}
-
+
+ {title}
+
+
{truncateRule(rule)}
-
+
);
};
-export function RulesPreview() {
+export function RulesSection() {
const ideMessenger = useContext(IdeMessengerContext);
const { selectedProfile } = useAuth();
@@ -65,10 +87,6 @@ export function RulesPreview() {
return (
-
- Assistant Rules
-
-
{mergedRules.length === 0 ? (
No rules defined yet
@@ -124,6 +142,7 @@ export function RulesPreview() {
})}
)}
+
);
}
diff --git a/gui/src/components/mainInput/Lump/sections/SelectedSection.tsx b/gui/src/components/mainInput/Lump/sections/SelectedSection.tsx
new file mode 100644
index 0000000000..28dc37c28d
--- /dev/null
+++ b/gui/src/components/mainInput/Lump/sections/SelectedSection.tsx
@@ -0,0 +1,32 @@
+import { ContextSection } from "./ContextSection";
+import DocsSection from "./docs/DocsSection";
+import { MCPSection } from "./MCPSection";
+import { ModelsSection } from "./ModelsSection";
+import { PromptsSection } from "./PromptsSection";
+import { RulesSection } from "./RulesSection";
+import { ToolsSection } from "./ToolsSection";
+
+interface SelectedSectionProps {
+ selectedSection: string | null;
+}
+
+export function SelectedSection(props: SelectedSectionProps) {
+ switch (props.selectedSection) {
+ case "models":
+ return
;
+ case "rules":
+ return
;
+ case "docs":
+ return
;
+ case "prompts":
+ return
;
+ case "context":
+ return
;
+ case "tools":
+ return
;
+ case "mcp":
+ return
;
+ default:
+ return null;
+ }
+}
diff --git a/gui/src/components/mainInput/Lump/sections/ToolsSection.tsx b/gui/src/components/mainInput/Lump/sections/ToolsSection.tsx
new file mode 100644
index 0000000000..af03f72204
--- /dev/null
+++ b/gui/src/components/mainInput/Lump/sections/ToolsSection.tsx
@@ -0,0 +1,7 @@
+import { ToolPermissionsDialog } from "../../InputToolbar/bottom/ToolPermissionsDialog";
+
+interface ToolsSectionProps {}
+
+export function ToolsSection({}: ToolsSectionProps) {
+ return
;
+}
diff --git a/gui/src/components/indexing/DocsDetailsDialog.tsx b/gui/src/components/mainInput/Lump/sections/docs/DocsDetailsDialog.tsx
similarity index 84%
rename from gui/src/components/indexing/DocsDetailsDialog.tsx
rename to gui/src/components/mainInput/Lump/sections/docs/DocsDetailsDialog.tsx
index f8d0a54e9d..2aa0698f09 100644
--- a/gui/src/components/indexing/DocsDetailsDialog.tsx
+++ b/gui/src/components/mainInput/Lump/sections/docs/DocsDetailsDialog.tsx
@@ -1,38 +1,15 @@
-import {
- CheckIcon,
- InformationCircleIcon,
- PencilIcon,
- PlusIcon,
-} from "@heroicons/react/24/outline";
-import {
- DocsIndexingDetails,
- IndexingStatus,
- PackageDocsResult,
- SiteIndexingConfig,
-} from "core";
-import preIndexedDocs from "core/indexing/docs/preIndexedDocs";
+import { DocsIndexingDetails } from "core";
import { usePostHog } from "posthog-js/react";
-import {
- useCallback,
- useContext,
- useEffect,
- useLayoutEffect,
- useMemo,
- useRef,
- useState,
-} from "react";
+import { useCallback, useContext, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
-import { Input, SecondaryButton } from "..";
-import { IdeMessengerContext } from "../../context/IdeMessenger";
-import { useAppSelector } from "../../redux/hooks";
-import { updateConfig } from "../../redux/slices/configSlice";
-import { updateIndexingStatus } from "../../redux/slices/indexingSlice";
-import { setDialogMessage, setShowDialog } from "../../redux/slices/uiSlice";
-import FileIcon from "../FileIcon";
-import { ToolTip } from "../gui/Tooltip";
-import DocsIndexingPeeks from "../indexing/DocsIndexingPeeks";
import { Tooltip } from "react-tooltip";
-
+import { SecondaryButton } from "../../../..";
+import { IdeMessengerContext } from "../../../../../context/IdeMessenger";
+import { useAppSelector } from "../../../../../redux/hooks";
+import {
+ setDialogMessage,
+ setShowDialog,
+} from "../../../../../redux/slices/uiSlice";
interface DocsDetailsDialogProps {
startUrl: string;
}
diff --git a/gui/src/components/indexing/DocsIndexingPeeks.tsx b/gui/src/components/mainInput/Lump/sections/docs/DocsIndexingPeeks.tsx
similarity index 92%
rename from gui/src/components/indexing/DocsIndexingPeeks.tsx
rename to gui/src/components/mainInput/Lump/sections/docs/DocsIndexingPeeks.tsx
index c218a5a1f1..bb2c75618e 100644
--- a/gui/src/components/indexing/DocsIndexingPeeks.tsx
+++ b/gui/src/components/mainInput/Lump/sections/docs/DocsIndexingPeeks.tsx
@@ -1,9 +1,11 @@
-import { useDispatch } from "react-redux";
import { IndexingStatus } from "core";
import { useMemo } from "react";
+import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
-import { setDialogMessage, setShowDialog } from "../../redux/slices/uiSlice";
-import { useAppSelector } from "../../redux/hooks";
+import {
+ setDialogMessage,
+ setShowDialog,
+} from "../../../../../redux/slices/uiSlice";
export interface DocsIndexingPeekProps {
status: IndexingStatus;
@@ -21,7 +23,7 @@ function DocsIndexingPeek({ status }: DocsIndexingPeekProps) {
{
- navigate("/more");
+ // navigate("/more"); TODO
dispatch(setShowDialog(false));
dispatch(setDialogMessage(undefined));
}}
diff --git a/gui/src/components/mainInput/Lump/sections/docs/DocsIndexingStatus.tsx b/gui/src/components/mainInput/Lump/sections/docs/DocsIndexingStatus.tsx
new file mode 100644
index 0000000000..28e764360f
--- /dev/null
+++ b/gui/src/components/mainInput/Lump/sections/docs/DocsIndexingStatus.tsx
@@ -0,0 +1,188 @@
+import { ConfigYaml } from "@continuedev/config-yaml";
+import {
+ ArrowPathIcon,
+ ArrowTopRightOnSquareIcon,
+ PencilIcon,
+ StopIcon,
+} from "@heroicons/react/24/outline";
+import { SiteIndexingConfig } from "core";
+import { useContext, useMemo, useState } from "react";
+import { useAuth } from "../../../../../context/Auth";
+import { IdeMessengerContext } from "../../../../../context/IdeMessenger";
+import { useAppDispatch, useAppSelector } from "../../../../../redux/hooks";
+import { updateIndexingStatus } from "../../../../../redux/slices/indexingSlice";
+import {
+ setDialogMessage,
+ setShowDialog,
+} from "../../../../../redux/slices/uiSlice";
+import { fontSize } from "../../../../../util";
+import ConfirmationDialog from "../../../../dialogs/ConfirmationDialog";
+import { StatusIndicator } from "./StatusIndicator";
+interface IndexingStatusViewerProps {
+ docConfig: SiteIndexingConfig;
+ docFromYaml?: NonNullable
[number];
+}
+
+function isUsesBlock(block: any): block is { uses: string } {
+ return typeof block !== "string" && "uses" in block;
+}
+
+function DocsIndexingStatus({
+ docConfig,
+ docFromYaml,
+}: IndexingStatusViewerProps) {
+ const ideMessenger = useContext(IdeMessengerContext);
+ const { selectedProfile } = useAuth();
+ const dispatch = useAppDispatch();
+
+ const status = useAppSelector(
+ (store) => store.indexing.indexing.statuses[docConfig.startUrl],
+ );
+
+ const reIndex = () =>
+ ideMessenger.post("indexing/reindex", {
+ type: "docs",
+ id: docConfig.startUrl,
+ });
+
+ const abort = () => {
+ ideMessenger.post("indexing/abort", {
+ type: "docs",
+ id: docConfig.startUrl,
+ });
+ // Optimistic abort status
+ if (status) {
+ dispatch(
+ updateIndexingStatus({ ...status, status: "aborted", progress: 0 }),
+ );
+ }
+ };
+
+ const [hasDeleted, setHasDeleted] = useState(false); // simple alternative to optimistic redux update
+ const onDelete = () => {
+ // optimistic update
+ dispatch(
+ setDialogMessage(
+ {
+ ideMessenger.post("context/removeDocs", {
+ startUrl: docConfig.startUrl,
+ });
+ setHasDeleted(true);
+ }}
+ />,
+ ),
+ );
+ dispatch(setShowDialog(true));
+ };
+
+ const openUrl = (path: string) =>
+ ideMessenger.request("controlPlane/openUrl", {
+ path,
+ orgSlug: undefined,
+ });
+
+ const handleEdit = () => {
+ console.log("edit", docFromYaml);
+ if (selectedProfile?.profileType === "local") {
+ ideMessenger.post("config/openProfile", {
+ profileId: undefined,
+ });
+ } else if (docFromYaml && isUsesBlock(docFromYaml)) {
+ openUrl(`${docFromYaml.uses}/new-version`);
+ } else if (selectedProfile?.fullSlug) {
+ const slug = `${selectedProfile.fullSlug.ownerSlug}/${selectedProfile.fullSlug.packageSlug}`;
+ openUrl(`${slug}/new-version`);
+ }
+ };
+ const progressPercentage = useMemo(() => {
+ if (!status) {
+ return 0;
+ }
+ return Math.min(100, Math.max(0, status.progress * 100)).toFixed(0);
+ }, [status?.progress]);
+
+ if (hasDeleted) return null;
+
+ return (
+
+
+
{
+ if (status?.url) {
+ ideMessenger.post("openUrl", status.url);
+ }
+ }}
+ >
+
+ {docConfig.faviconUrl ? (
+
+ ) : null}
+
+ {docConfig.title ?? docConfig.startUrl}
+
+
+
+
+
+
+ {status?.status === "indexing" && (
+
+ {progressPercentage}%
+
+ )}
+
+ {status?.status === "indexing" && (
+
+ )}
+
+ {["aborted", "complete", "failed"].includes(
+ status?.status ?? "",
+ ) && (
+
+ )}
+
+
+
+ {/* Removed StatusIndicator from here */}
+
+
+
+
+ );
+}
+
+export default DocsIndexingStatus;
diff --git a/gui/src/components/mainInput/Lump/sections/docs/DocsSection.tsx b/gui/src/components/mainInput/Lump/sections/docs/DocsSection.tsx
new file mode 100644
index 0000000000..4349c94981
--- /dev/null
+++ b/gui/src/components/mainInput/Lump/sections/docs/DocsSection.tsx
@@ -0,0 +1,78 @@
+import { parseConfigYaml } from "@continuedev/config-yaml";
+import { IndexingStatus } from "core";
+import { useMemo } from "react";
+import { useDispatch } from "react-redux";
+import { useAuth } from "../../../../../context/Auth";
+import { useAppSelector } from "../../../../../redux/hooks";
+import { AddBlockButton } from "../AddBlockButton";
+import DocsIndexingStatus from "./DocsIndexingStatus";
+
+function DocsIndexingStatuses() {
+ const dispatch = useDispatch();
+ const config = useAppSelector((store) => store.config.config);
+ const indexingStatuses = useAppSelector(
+ (store) => store.indexing.indexing.statuses,
+ );
+ const { selectedProfile } = useAuth();
+
+ const mergedDocs = useMemo(() => {
+ const parsed = selectedProfile?.rawYaml
+ ? parseConfigYaml(selectedProfile?.rawYaml ?? "")
+ : undefined;
+ return (config.docs ?? []).map((doc, index) => ({
+ doc,
+ docFromYaml: parsed?.docs?.[index],
+ }));
+ }, [config.docs, selectedProfile?.rawYaml]);
+
+ const sortedConfigDocs = useMemo(() => {
+ const sorter = (status: IndexingStatus["status"]) => {
+ if (status === "complete") return 0;
+ if (status === "indexing" || status === "paused") return 1;
+ if (status === "failed") return 2;
+ if (status === "aborted" || status === "pending") return 3;
+ return 4;
+ };
+
+ const docs = [...mergedDocs];
+ docs.sort((a, b) => {
+ const statusA = indexingStatuses[a.doc.startUrl]?.status ?? "pending";
+ const statusB = indexingStatuses[b.doc.startUrl]?.status ?? "pending";
+
+ // First, compare by status
+ const statusCompare = sorter(statusA) - sorter(statusB);
+ if (statusCompare !== 0) return statusCompare;
+
+ // If status is the same, sort by presence of icon
+ const hasIconA = !!a.doc.faviconUrl;
+ const hasIconB = !!b.doc.faviconUrl;
+ return hasIconB === hasIconA ? 0 : hasIconB ? 1 : -1;
+ });
+ return docs;
+ }, [mergedDocs, indexingStatuses]);
+
+ return (
+
+
+ {sortedConfigDocs.map((docConfig) => {
+ return (
+
+ );
+ })}
+
+
+
+ );
+}
+
+export default DocsIndexingStatuses;
diff --git a/gui/src/components/mainInput/Lump/sections/docs/StatusIndicator.tsx b/gui/src/components/mainInput/Lump/sections/docs/StatusIndicator.tsx
new file mode 100644
index 0000000000..d6dd2d55a3
--- /dev/null
+++ b/gui/src/components/mainInput/Lump/sections/docs/StatusIndicator.tsx
@@ -0,0 +1,44 @@
+import { IndexingStatus } from "core";
+import { ToolTip } from "../../../../gui/Tooltip";
+
+interface StatusIndicatorProps {
+ status?: IndexingStatus["status"];
+ className?: string;
+ size?: number;
+ hoverMessage?: string;
+}
+
+const STATUS_TO_COLOR: Record = {
+ indexing: "bg-yellow-500",
+ paused: "bg-blue-500",
+ complete: "bg-green-500",
+ aborted: "bg-gray-500",
+ pending: "bg-gray-300",
+ failed: "bg-red-500",
+};
+
+export function StatusIndicator({
+ status,
+ className = "",
+ size = 2,
+ hoverMessage,
+}: StatusIndicatorProps) {
+ if (!status) return null;
+
+ const indicator = (
+
+ );
+
+ return (
+ <>
+ {indicator}
+ {hoverMessage && }
+ >
+ );
+}
diff --git a/gui/src/components/mainInput/TipTapEditor.tsx b/gui/src/components/mainInput/TipTapEditor.tsx
deleted file mode 100644
index 06261d1b97..0000000000
--- a/gui/src/components/mainInput/TipTapEditor.tsx
+++ /dev/null
@@ -1,1035 +0,0 @@
-import Document from "@tiptap/extension-document";
-import History from "@tiptap/extension-history";
-import Image from "@tiptap/extension-image";
-import Paragraph from "@tiptap/extension-paragraph";
-import Placeholder from "@tiptap/extension-placeholder";
-import Text from "@tiptap/extension-text";
-import { Plugin } from "@tiptap/pm/state";
-import { Editor, EditorContent, JSONContent, useEditor } from "@tiptap/react";
-import { ContextProviderDescription, InputModifiers } from "core";
-import { rifWithContentsToContextItem } from "core/commands/util";
-import { modelSupportsImages } from "core/llm/autodetect";
-import { debounce } from "lodash";
-import { usePostHog } from "posthog-js/react";
-import {
- KeyboardEvent,
- useCallback,
- useContext,
- useEffect,
- useRef,
- useState,
-} from "react";
-import styled from "styled-components";
-import {
- defaultBorderRadius,
- lightGray,
- vscBadgeBackground,
- vscCommandCenterActiveBorder,
- vscCommandCenterInactiveBorder,
- vscForeground,
- vscInputBackground,
- vscInputBorderFocus,
-} from "..";
-import { IdeMessengerContext } from "../../context/IdeMessenger";
-import { useSubmenuContextProviders } from "../../context/SubmenuContextProviders";
-import { useInputHistory } from "../../hooks/useInputHistory";
-import useIsOSREnabled from "../../hooks/useIsOSREnabled";
-import useUpdatingRef from "../../hooks/useUpdatingRef";
-import { useWebviewListener } from "../../hooks/useWebviewListener";
-import { useAppDispatch, useAppSelector } from "../../redux/hooks";
-import { selectUseActiveFile } from "../../redux/selectors";
-import { selectDefaultModel } from "../../redux/slices/configSlice";
-import {
- addCodeToEdit,
- clearCodeToEdit,
- selectHasCodeToEdit,
- selectIsInEditMode,
- setMainEditorContentTrigger,
- setNewestCodeblocksForInput,
-} from "../../redux/slices/sessionSlice";
-import { exitEditMode } from "../../redux/thunks";
-import {
- loadLastSession,
- loadSession,
- saveCurrentSession,
-} from "../../redux/thunks/session";
-import {
- getFontSize,
- isJetBrains,
- isMetaEquivalentKeyPressed,
-} from "../../util";
-import { AddCodeToEdit } from "./AddCodeToEditExtension";
-import { CodeBlockExtension } from "./CodeBlockExtension";
-import { SlashCommand } from "./CommandsExtension";
-import { MockExtension } from "./FillerExtension";
-import InputToolbar, { ToolbarOptions } from "./InputToolbar";
-import { Mention } from "./MentionExtension";
-import "./TipTapEditor.css";
-import {
- getContextProviderDropdownOptions,
- getSlashCommandDropdownOptions,
-} from "./getSuggestion";
-import {
- handleJetBrainsOSRMetaKeyIssues,
- handleVSCMetaKeyIssues,
-} from "./handleMetaKeyIssues";
-import { ComboBoxItem } from "./types";
-
-const InputBoxDiv = styled.div<{}>`
- resize: none;
- padding-bottom: 4px;
- font-family: inherit;
- border-radius: ${defaultBorderRadius};
- margin: 0;
- height: auto;
- width: 100%;
- background-color: ${vscInputBackground};
- color: ${vscForeground};
-
- border: 1px solid ${vscCommandCenterInactiveBorder};
- transition: border-color 0.15s ease-in-out;
- &:focus-within {
- border: 1px solid ${vscCommandCenterActiveBorder};
- }
-
- outline: none;
- font-size: ${getFontSize()}px;
-
- &:focus {
- outline: none;
-
- border: 0.5px solid ${vscInputBorderFocus};
- }
-
- &::placeholder {
- color: ${lightGray}cc;
- }
-
- display: flex;
- flex-direction: column;
-`;
-
-const HoverDiv = styled.div`
- position: absolute;
- width: 100%;
- height: 100%;
- top: 0;
- left: 0;
- opacity: 0.5;
- background-color: ${vscBadgeBackground};
- color: ${vscForeground};
- display: flex;
- align-items: center;
- justify-content: center;
-`;
-
-const HoverTextDiv = styled.div`
- position: absolute;
- width: 100%;
- height: 100%;
- top: 0;
- left: 0;
- color: ${vscForeground};
- display: flex;
- align-items: center;
- justify-content: center;
-`;
-
-const IMAGE_RESOLUTION = 1024;
-function getDataUrlForFile(
- file: File,
- img: HTMLImageElement,
-): string | undefined {
- const targetWidth = IMAGE_RESOLUTION;
- const targetHeight = IMAGE_RESOLUTION;
- const scaleFactor = Math.min(
- targetWidth / img.width,
- targetHeight / img.height,
- );
-
- const canvas = document.createElement("canvas");
- canvas.width = img.width * scaleFactor;
- canvas.height = img.height * scaleFactor;
-
- const ctx = canvas.getContext("2d");
- if (!ctx) {
- console.error("Error getting image data url: 2d context not found");
- return;
- }
- ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
-
- const downsizedDataUrl = canvas.toDataURL("image/jpeg", 0.7);
- return downsizedDataUrl;
-}
-
-interface TipTapEditorProps {
- availableContextProviders: ContextProviderDescription[];
- availableSlashCommands: ComboBoxItem[];
- isMainInput: boolean;
- onEnter: (
- editorState: JSONContent,
- modifiers: InputModifiers,
- editor: Editor,
- ) => void;
- editorState?: JSONContent;
- toolbarOptions?: ToolbarOptions;
- placeholder?: string;
- historyKey: string;
- inputId: string;
-}
-
-export const TIPPY_DIV_ID = "tippy-js-div";
-
-function TipTapEditor(props: TipTapEditorProps) {
- const dispatch = useAppDispatch();
-
- const ideMessenger = useContext(IdeMessengerContext);
- const { getSubmenuContextItems } = useSubmenuContextProviders();
-
- const historyLength = useAppSelector((store) => store.session.history.length);
-
- const useActiveFile = useAppSelector(selectUseActiveFile);
-
- const posthog = usePostHog();
-
- const inSubmenuRef = useRef(undefined);
- const inDropdownRef = useRef(false);
-
- const isOSREnabled = useIsOSREnabled();
-
- const enterSubmenu = async (editor: Editor, providerId: string) => {
- const contents = editor.getText();
- const indexOfAt = contents.lastIndexOf("@");
- if (indexOfAt === -1) {
- return;
- }
-
- // Find the position of the last @ character
- // We do this because editor.getText() isn't a correct representation including node views
- let startPos = editor.state.selection.anchor;
- while (
- startPos > 0 &&
- editor.state.doc.textBetween(startPos, startPos + 1) !== "@"
- ) {
- startPos--;
- }
- startPos++;
-
- editor.commands.deleteRange({
- from: startPos,
- to: editor.state.selection.anchor,
- });
- inSubmenuRef.current = providerId;
-
- // to trigger refresh of suggestions
- editor.commands.insertContent(":");
- editor.commands.deleteRange({
- from: editor.state.selection.anchor - 1,
- to: editor.state.selection.anchor,
- });
- };
-
- const onClose = () => {
- inSubmenuRef.current = undefined;
- inDropdownRef.current = false;
- };
-
- const onOpen = () => {
- inDropdownRef.current = true;
- };
-
- const defaultModel = useAppSelector(selectDefaultModel);
- const defaultModelRef = useUpdatingRef(defaultModel);
-
- const getSubmenuContextItemsRef = useUpdatingRef(getSubmenuContextItems);
- const availableContextProvidersRef = useUpdatingRef(
- props.availableContextProviders,
- );
-
- const historyLengthRef = useUpdatingRef(historyLength);
- const availableSlashCommandsRef = useUpdatingRef(
- props.availableSlashCommands,
- );
-
- const isStreaming = useAppSelector((state) => state.session.isStreaming);
- const isStreamingRef = useUpdatingRef(isStreaming);
-
- const isInEditMode = useAppSelector(selectIsInEditMode);
- const isInEditModeRef = useUpdatingRef(isInEditMode);
- const hasCodeToEdit = useAppSelector(selectHasCodeToEdit);
- const isEditModeAndNoCodeToEdit = isInEditMode && !hasCodeToEdit;
- async function handleImageFile(
- file: File,
- ): Promise<[HTMLImageElement, string] | undefined> {
- let filesize = file.size / 1024 / 1024; // filesize in MB
- // check image type and size
- if (
- [
- "image/jpeg",
- "image/jpg",
- "image/png",
- "image/gif",
- "image/svg",
- "image/webp",
- ].includes(file.type) &&
- filesize < 10
- ) {
- // check dimensions
- let _URL = window.URL || window.webkitURL;
- let img = new window.Image();
- img.src = _URL.createObjectURL(file);
-
- return await new Promise((resolve) => {
- img.onload = function () {
- const dataUrl = getDataUrlForFile(file, img);
- if (!dataUrl) {
- return;
- }
-
- let image = new window.Image();
- image.src = dataUrl;
- image.onload = function () {
- resolve([image, dataUrl]);
- };
- };
- });
- } else {
- ideMessenger.post("showToast", [
- "error",
- "Images need to be in jpg or png format and less than 10MB in size.",
- ]);
- }
- }
-
- const { prevRef, nextRef, addRef } = useInputHistory(props.historyKey);
-
- const editor: Editor | null = useEditor({
- extensions: [
- Document,
- History,
- Image.extend({
- addProseMirrorPlugins() {
- const plugin = new Plugin({
- props: {
- handleDOMEvents: {
- paste(view, event) {
- const model = defaultModelRef.current;
- if (!model) return;
- const items = event.clipboardData?.items;
- if (items) {
- for (const item of items) {
- const file = item.getAsFile();
- file &&
- modelSupportsImages(
- model.provider,
- model.model,
- model.title,
- model.capabilities,
- ) &&
- handleImageFile(file).then((resp) => {
- if (!resp) return;
- const [img, dataUrl] = resp;
- const { schema } = view.state;
- const node = schema.nodes.image.create({
- src: dataUrl,
- });
- const tr = view.state.tr.insert(0, node);
- view.dispatch(tr);
- });
- }
- }
- },
- },
- },
- });
- return [plugin];
- },
- }).configure({
- HTMLAttributes: {
- class: "object-contain max-h-[210px] max-w-full mx-1",
- },
- }),
- Placeholder.configure({
- placeholder: getPlaceholderText(
- props.placeholder,
- historyLengthRef.current,
- ),
- }),
- Paragraph.extend({
- addKeyboardShortcuts() {
- return {
- Enter: () => {
- if (inDropdownRef.current) {
- return false;
- }
-
- onEnterRef.current({
- useCodebase: false,
- noContext: !useActiveFile,
- });
- return true;
- },
-
- "Mod-Enter": () => {
- onEnterRef.current({
- useCodebase: true,
- noContext: !useActiveFile,
- });
- return true;
- },
- "Alt-Enter": () => {
- posthog.capture("gui_use_active_file_enter");
-
- onEnterRef.current({
- useCodebase: false,
- noContext: !!useActiveFile,
- });
-
- return true;
- },
- "Mod-Backspace": () => {
- // If you press cmd+backspace wanting to cancel,
- // but are inside of a text box, it shouldn't
- // delete the text
- if (isStreamingRef.current) {
- return true;
- }
- return false;
- },
- "Shift-Enter": () =>
- this.editor.commands.first(({ commands }) => [
- () => commands.newlineInCode(),
- () => commands.createParagraphNear(),
- () => commands.liftEmptyBlock(),
- () => commands.splitBlock(),
- ]),
-
- ArrowUp: () => {
- if (this.editor.state.selection.anchor > 1) {
- return false;
- }
-
- const previousInput = prevRef.current(
- this.editor.state.toJSON().doc,
- );
- if (previousInput) {
- this.editor.commands.setContent(previousInput);
- setTimeout(() => {
- this.editor.commands.blur();
- this.editor.commands.focus("start");
- }, 0);
- return true;
- }
- return false;
- },
- Escape: () => {
- if (inDropdownRef.current || !isInEditModeRef.current) {
- ideMessenger.post("focusEditor", undefined);
- return true;
- }
- (async () => {
- await dispatch(
- loadLastSession({
- saveCurrentSession: false,
- }),
- );
- dispatch(exitEditMode());
- })();
-
- return true;
- },
- ArrowDown: () => {
- if (
- this.editor.state.selection.anchor <
- this.editor.state.doc.content.size - 1
- ) {
- return false;
- }
- const nextInput = nextRef.current();
- if (nextInput) {
- this.editor.commands.setContent(nextInput);
- setTimeout(() => {
- this.editor.commands.blur();
- this.editor.commands.focus("end");
- }, 0);
- return true;
- }
- return false;
- },
- };
- },
- }).configure({
- HTMLAttributes: {
- class: "my-1",
- },
- }),
- Text,
- Mention.configure({
- HTMLAttributes: {
- class: "mention",
- },
- suggestion: getContextProviderDropdownOptions(
- availableContextProvidersRef,
- getSubmenuContextItemsRef,
- enterSubmenu,
- onClose,
- onOpen,
- inSubmenuRef,
- ideMessenger,
- ),
- renderHTML: (props) => {
- return `@${props.node.attrs.label || props.node.attrs.id}`;
- },
- }),
-
- AddCodeToEdit.configure({
- HTMLAttributes: {
- class: "add-code-to-edit",
- },
- suggestion: {
- ...getContextProviderDropdownOptions(
- availableContextProvidersRef,
- getSubmenuContextItemsRef,
- enterSubmenu,
- onClose,
- onOpen,
- inSubmenuRef,
- ideMessenger,
- ),
- allow: () => isInEditModeRef.current,
- command: async ({ editor, range, props }) => {
- editor.chain().focus().insertContentAt(range, "").run();
- const filepath = props.id;
- const contents = await ideMessenger.ide.readFile(filepath);
- dispatch(
- addCodeToEdit({
- filepath,
- contents,
- }),
- );
- },
- items: async ({ query }) => {
- // Only display files in the dropdown
- const results = getSubmenuContextItemsRef.current("file", query);
- return results.map((result) => ({
- ...result,
- label: result.title,
- type: "file",
- query: result.id,
- icon: result.icon,
- }));
- },
- },
- }),
- props.availableSlashCommands.length
- ? SlashCommand.configure({
- HTMLAttributes: {
- class: "mention",
- },
- suggestion: getSlashCommandDropdownOptions(
- availableSlashCommandsRef,
- onClose,
- onOpen,
- ideMessenger,
- ),
- renderText: (props) => {
- return props.node.attrs.label;
- },
- })
- : MockExtension,
- CodeBlockExtension,
- ],
- editorProps: {
- attributes: {
- class: "outline-none -mt-1 overflow-hidden",
- style: `font-size: ${getFontSize()}px;`,
- },
- },
- content: props.editorState,
- editable: !isStreaming || props.isMainInput,
- });
-
- const [shouldHideToolbar, setShouldHideToolbar] = useState(false);
- const debouncedShouldHideToolbar = debounce((value) => {
- setShouldHideToolbar(value);
- }, 200);
-
- function getPlaceholderText(
- placeholder: TipTapEditorProps["placeholder"],
- historyLength: number,
- ) {
- if (placeholder) {
- return placeholder;
- }
-
- return historyLength === 0
- ? "Ask anything, '/' for prompts, '@' to add context"
- : "Ask a follow-up";
- }
-
- useEffect(() => {
- if (!editor) {
- return;
- }
- const placeholder = getPlaceholderText(
- props.placeholder,
- historyLengthRef.current,
- );
-
- editor.extensionManager.extensions.filter(
- (extension) => extension.name === "placeholder",
- )[0].options["placeholder"] = placeholder;
-
- editor.view.dispatch(editor.state.tr);
- }, [editor, props.placeholder, historyLengthRef.current]);
-
- useEffect(() => {
- if (props.isMainInput) {
- editor?.commands.clearContent(true);
- }
- }, [editor, isInEditMode, props.isMainInput]);
-
- useEffect(() => {
- if (editor) {
- const handleFocus = () => {
- setShouldHideToolbar(false);
- };
-
- const handleBlur = () => {
- // TODO - make toolbar auto-hiding work without breaking tool dropdown focus
- // debouncedShouldHideToolbar(true);
- };
-
- editor.on("focus", handleFocus);
- editor.on("blur", handleBlur);
-
- return () => {
- editor.off("focus", handleFocus);
- editor.off("blur", handleBlur);
- };
- }
- }, [editor]);
-
- const editorFocusedRef = useUpdatingRef(editor?.isFocused, [editor]);
-
- /**
- * This handles various issues with meta key actions
- * - In JetBrains, when using OSR in JCEF, there is a bug where using the meta key to
- * highlight code using arrow keys is not working
- * - In VS Code, while working with .ipynb files there is a problem where copy/paste/cut will affect
- * the actual notebook cells, even when performing them in our GUI
- *
- * Currently keydown events for a number of keys are not registering if the
- * meta/shift key is pressed, for example "x", "c", "v", "z", etc.
- * Until this is resolved we can't turn on OSR for non-Mac users due to issues
- * with those key actions.
- */
- const handleKeyDown = async (e: KeyboardEvent) => {
- if (!editor) {
- return;
- }
-
- setActiveKey(e.key);
-
- if (!editorFocusedRef?.current || !isMetaEquivalentKeyPressed(e)) return;
-
- if (isOSREnabled) {
- handleJetBrainsOSRMetaKeyIssues(e, editor);
- } else if (!isJetBrains()) {
- await handleVSCMetaKeyIssues(e, editor);
- }
- };
-
- const handleKeyUp = () => {
- setActiveKey(null);
- };
-
- const onEnterRef = useUpdatingRef(
- (modifiers: InputModifiers) => {
- if (!editor) {
- return;
- }
- if (isStreaming || isEditModeAndNoCodeToEdit) {
- return;
- }
-
- const json = editor.getJSON();
-
- // Don't do anything if input box is empty
- if (!json.content?.some((c) => c.content)) {
- return;
- }
-
- if (props.isMainInput) {
- addRef.current(json);
- }
-
- props.onEnter(json, modifiers, editor);
- },
- [props.onEnter, editor, props.isMainInput],
- );
-
- useEffect(() => {
- if (props.isMainInput) {
- /**
- * I have a strong suspicion that many of the other focus
- * commands are redundant, especially the ones inside
- * useTimeout.
- */
- editor?.commands.focus();
- }
- }, [props.isMainInput, editor]);
-
- // Re-focus main input after done generating
- useEffect(() => {
- if (editor && !isStreaming && props.isMainInput && document.hasFocus()) {
- editor.commands.focus(undefined, { scrollIntoView: false });
- }
- }, [props.isMainInput, isStreaming, editor]);
-
- // This allows anywhere in the app to set the content of the main input
- const mainInputContentTrigger = useAppSelector(
- (store) => store.session.mainEditorContentTrigger,
- );
- useEffect(() => {
- if (!props.isMainInput || !mainInputContentTrigger) {
- return;
- }
- queueMicrotask(() => {
- editor?.commands.setContent(mainInputContentTrigger);
- });
- dispatch(setMainEditorContentTrigger(undefined));
- }, [editor, props.isMainInput, mainInputContentTrigger]);
-
- // IDE event listeners
- useWebviewListener(
- "userInput",
- async (data) => {
- if (!props.isMainInput) {
- return;
- }
- editor?.commands.insertContent(data.input);
- onEnterRef.current({ useCodebase: false, noContext: true });
- },
- [editor, onEnterRef.current, props.isMainInput],
- );
-
- useWebviewListener("jetbrains/editorInsetRefresh", async () => {
- editor?.chain().clearContent().focus().run();
- });
-
- useWebviewListener(
- "focusContinueInput",
- async (data) => {
- if (!props.isMainInput) {
- return;
- }
-
- dispatch(clearCodeToEdit());
-
- if (historyLength > 0) {
- await dispatch(
- saveCurrentSession({
- openNewSession: false,
- generateTitle: true,
- }),
- );
- }
- setTimeout(() => {
- editor?.commands.blur();
- editor?.commands.focus("end");
- }, 20);
- },
- [historyLength, editor, props.isMainInput],
- );
-
- useWebviewListener(
- "focusContinueInputWithoutClear",
- async () => {
- if (!props.isMainInput) {
- return;
- }
- setTimeout(() => {
- editor?.commands.focus("end");
- }, 20);
- },
- [editor, props.isMainInput],
- );
-
- useWebviewListener(
- "focusContinueInputWithNewSession",
- async () => {
- if (!props.isMainInput) {
- return;
- }
- await dispatch(
- saveCurrentSession({
- openNewSession: true,
- generateTitle: true,
- }),
- );
- setTimeout(() => {
- editor?.commands.focus("end");
- }, 20);
- },
- [editor, props.isMainInput],
- );
-
- useWebviewListener(
- "highlightedCode",
- async (data) => {
- if (!props.isMainInput || !editor) {
- return;
- }
-
- const contextItem = rifWithContentsToContextItem(
- data.rangeInFileWithContents,
- );
-
- let index = 0;
- for (const el of editor.getJSON()?.content ?? []) {
- if (el.attrs?.item?.name === contextItem.name) {
- return; // Prevent exact duplicate code blocks
- }
- if (el.type === "codeBlock") {
- index += 2;
- } else {
- break;
- }
- }
- editor
- .chain()
- .insertContentAt(index, {
- type: "codeBlock",
- attrs: {
- item: contextItem,
- inputId: props.inputId,
- },
- })
- .run();
- dispatch(
- setNewestCodeblocksForInput({
- inputId: props.inputId,
- contextItemId: contextItem.id.itemId,
- }),
- );
- if (data.prompt) {
- editor.commands.focus("end");
- editor.commands.insertContent(data.prompt);
- }
-
- if (data.shouldRun) {
- onEnterRef.current({ useCodebase: false, noContext: true });
- }
-
- setTimeout(() => {
- editor.commands.blur();
- editor.commands.focus("end");
- }, 20);
- },
- [editor, props.isMainInput, historyLength, onEnterRef.current],
- );
-
- useWebviewListener(
- "focusEdit",
- async () => {
- if (!props.isMainInput) {
- return;
- }
-
- setTimeout(() => {
- editor?.commands.focus("end");
- }, 20);
- },
- [editor, props.isMainInput],
- );
-
- useWebviewListener(
- "focusEditWithoutClear",
- async () => {
- if (!props.isMainInput) {
- return;
- }
-
- setTimeout(() => {
- editor?.commands.focus("end");
- }, 2000);
- },
- [editor, props.isMainInput],
- );
-
- useWebviewListener(
- "isContinueInputFocused",
- async () => {
- return props.isMainInput && !!editorFocusedRef.current;
- },
- [editorFocusedRef, props.isMainInput],
- !props.isMainInput,
- );
-
- useWebviewListener(
- "focusContinueSessionId",
- async (data) => {
- if (!props.isMainInput || !data.sessionId) {
- return;
- }
- await dispatch(
- loadSession({
- sessionId: data.sessionId,
- saveCurrentSession: true,
- }),
- );
- },
- [props.isMainInput],
- );
-
- const [showDragOverMsg, setShowDragOverMsg] = useState(false);
-
- useEffect(() => {
- const overListener = (event: DragEvent) => {
- if (event.shiftKey) return;
- setShowDragOverMsg(true);
- };
- window.addEventListener("dragover", overListener);
-
- const leaveListener = (event: DragEvent) => {
- if (event.shiftKey) {
- setShowDragOverMsg(false);
- } else {
- setTimeout(() => setShowDragOverMsg(false), 2000);
- }
- };
- window.addEventListener("dragleave", leaveListener);
-
- return () => {
- window.removeEventListener("dragover", overListener);
- window.removeEventListener("dragleave", leaveListener);
- };
- }, []);
-
- const [activeKey, setActiveKey] = useState(null);
-
- const insertCharacterWithWhitespace = useCallback(
- (char: string) => {
- if (!editor) {
- return;
- }
- const text = editor.getText();
- if (!text.endsWith(char)) {
- if (text.length > 0 && !text.endsWith(" ")) {
- editor.commands.insertContent(` ${char}`);
- } else {
- editor.commands.insertContent(char);
- }
- }
- },
- [editor],
- );
-
- return (
- {
- editor?.commands.focus();
- }}
- onDragOver={(event) => {
- event.preventDefault();
- setShowDragOverMsg(true);
- }}
- onDragLeave={(e) => {
- if (e.relatedTarget === null) {
- if (e.shiftKey) {
- setShowDragOverMsg(false);
- } else {
- setTimeout(() => setShowDragOverMsg(false), 2000);
- }
- }
- }}
- onDragEnter={() => {
- setShowDragOverMsg(true);
- }}
- onDrop={(event) => {
- if (
- !defaultModel ||
- !modelSupportsImages(
- defaultModel.provider,
- defaultModel.model,
- defaultModel.title,
- defaultModel.capabilities,
- )
- ) {
- return;
- }
- setShowDragOverMsg(false);
- let file = event.dataTransfer.files[0];
- handleImageFile(file).then((result) => {
- if (!editor) {
- return;
- }
- if (result) {
- const [_, dataUrl] = result;
- const { schema } = editor.state;
- const node = schema.nodes.image.create({ src: dataUrl });
- const tr = editor.state.tr.insert(0, node);
- editor.view.dispatch(tr);
- }
- });
- event.preventDefault();
- }}
- >
-
- {
- event.stopPropagation();
- }}
- />
- insertCharacterWithWhitespace("@")}
- onEnter={onEnterRef.current}
- onImageFileSelected={(file) => {
- handleImageFile(file).then((result) => {
- if (!editor) {
- return;
- }
- if (result) {
- const [_, dataUrl] = result;
- const { schema } = editor.state;
- const node = schema.nodes.image.create({ src: dataUrl });
- editor.commands.command(({ tr }) => {
- tr.insert(0, node);
- return true;
- });
- }
- });
- }}
- disabled={isStreaming}
- />
-
-
- {showDragOverMsg &&
- modelSupportsImages(
- defaultModel?.provider || "",
- defaultModel?.model || "",
- defaultModel?.title,
- defaultModel?.capabilities,
- ) && (
- <>
-
- Hold ⇧ to drop image
- >
- )}
-
-
- );
-}
-
-export default TipTapEditor;
diff --git a/gui/src/components/mainInput/ContextItemsPeek.tsx b/gui/src/components/mainInput/belowMainInput/ContextItemsPeek.tsx
similarity index 93%
rename from gui/src/components/mainInput/ContextItemsPeek.tsx
rename to gui/src/components/mainInput/belowMainInput/ContextItemsPeek.tsx
index 9cd140d2c9..eb63e0ee9d 100644
--- a/gui/src/components/mainInput/ContextItemsPeek.tsx
+++ b/gui/src/components/mainInput/belowMainInput/ContextItemsPeek.tsx
@@ -7,14 +7,14 @@ import { ContextItemWithId } from "core";
import { ctxItemToRifWithContents } from "core/commands/util";
import { getUriPathBasename } from "core/util/uri";
import { useContext, useMemo, useState } from "react";
-import { AnimatedEllipsis, lightGray, vscBackground } from "..";
-import { IdeMessengerContext } from "../../context/IdeMessenger";
-import { useAppSelector } from "../../redux/hooks";
-import { selectIsGatheringContext } from "../../redux/slices/sessionSlice";
-import FileIcon from "../FileIcon";
-import SafeImg from "../SafeImg";
-import { getIconFromDropdownItem } from "./MentionList";
-import { NAMED_ICONS } from "./icons";
+import { AnimatedEllipsis, lightGray, vscBackground } from "../..";
+import { IdeMessengerContext } from "../../../context/IdeMessenger";
+import { useAppSelector } from "../../../redux/hooks";
+import { selectIsGatheringContext } from "../../../redux/slices/sessionSlice";
+import FileIcon from "../../FileIcon";
+import SafeImg from "../../SafeImg";
+import { getIconFromDropdownItem } from "../AtMentionDropdown";
+import { NAMED_ICONS } from "../icons";
interface ContextItemsPeekProps {
contextItems?: ContextItemWithId[];
diff --git a/gui/src/components/mainInput/NewSessionButton.tsx b/gui/src/components/mainInput/belowMainInput/NewSessionButton.tsx
similarity index 90%
rename from gui/src/components/mainInput/NewSessionButton.tsx
rename to gui/src/components/mainInput/belowMainInput/NewSessionButton.tsx
index 04193450ca..d8623af968 100644
--- a/gui/src/components/mainInput/NewSessionButton.tsx
+++ b/gui/src/components/mainInput/belowMainInput/NewSessionButton.tsx
@@ -1,6 +1,6 @@
import styled from "styled-components";
-import { defaultBorderRadius, lightGray, vscForeground } from "..";
-import { getFontSize } from "../../util";
+import { defaultBorderRadius, lightGray, vscForeground } from "../..";
+import { getFontSize } from "../../../util";
export const NewSessionButton = styled.div`
width: fit-content;
diff --git a/gui/src/components/mainInput/ThinkingBlockPeek.tsx b/gui/src/components/mainInput/belowMainInput/ThinkingBlockPeek.tsx
similarity index 97%
rename from gui/src/components/mainInput/ThinkingBlockPeek.tsx
rename to gui/src/components/mainInput/belowMainInput/ThinkingBlockPeek.tsx
index 8516352300..a89ba2e6a1 100644
--- a/gui/src/components/mainInput/ThinkingBlockPeek.tsx
+++ b/gui/src/components/mainInput/belowMainInput/ThinkingBlockPeek.tsx
@@ -4,9 +4,9 @@ import { ChevronUpIcon } from "@heroicons/react/24/solid";
import { ChatHistoryItem } from "core";
import { useEffect, useState } from "react";
import styled from "styled-components";
-import { defaultBorderRadius, lightGray, vscBackground } from "..";
-import { getFontSize } from "../../util";
-import StyledMarkdownPreview from "../markdown/StyledMarkdownPreview";
+import { defaultBorderRadius, lightGray, vscBackground } from "../..";
+import { getFontSize } from "../../../util";
+import StyledMarkdownPreview from "../../markdown/StyledMarkdownPreview";
const SpoilerButton = styled.div`
background-color: ${vscBackground};
diff --git a/gui/src/components/mainInput/CodeBlockComponent.tsx b/gui/src/components/mainInput/tiptap/CodeBlockComponent.tsx
similarity index 92%
rename from gui/src/components/mainInput/CodeBlockComponent.tsx
rename to gui/src/components/mainInput/tiptap/CodeBlockComponent.tsx
index 61403a2610..e63e273ada 100644
--- a/gui/src/components/mainInput/CodeBlockComponent.tsx
+++ b/gui/src/components/mainInput/tiptap/CodeBlockComponent.tsx
@@ -1,7 +1,7 @@
import { NodeViewWrapper, NodeViewWrapperProps } from "@tiptap/react";
import { ContextItemWithId } from "core";
-import { vscBadgeBackground } from "..";
-import CodeSnippetPreview from "../markdown/CodeSnippetPreview";
+import { vscBadgeBackground } from "../..";
+import CodeSnippetPreview from "../../markdown/CodeSnippetPreview";
export const CodeBlockComponent = (props: any) => {
const { node, deleteNode, selected, editor, updateAttributes } = props;
diff --git a/gui/src/components/mainInput/tiptap/DragOverlay.tsx b/gui/src/components/mainInput/tiptap/DragOverlay.tsx
new file mode 100644
index 0000000000..1417a82281
--- /dev/null
+++ b/gui/src/components/mainInput/tiptap/DragOverlay.tsx
@@ -0,0 +1,40 @@
+import React, { useEffect } from "react";
+import { HoverDiv, HoverTextDiv } from "./StyledComponents";
+
+interface DragOverlayProps {
+ show: boolean;
+ setShow: (show: boolean) => void;
+}
+
+export const DragOverlay: React.FC = ({ show, setShow }) => {
+ useEffect(() => {
+ const overListener = (event: DragEvent) => {
+ if (event.shiftKey) return;
+ setShow(true);
+ };
+ window.addEventListener("dragover", overListener);
+
+ const leaveListener = (event: DragEvent) => {
+ if (event.shiftKey) {
+ setShow(false);
+ } else {
+ setTimeout(() => setShow(false), 2000);
+ }
+ };
+ window.addEventListener("dragleave", leaveListener);
+
+ return () => {
+ window.removeEventListener("dragover", overListener);
+ window.removeEventListener("dragleave", leaveListener);
+ };
+ }, []);
+
+ if (!show) return null;
+
+ return (
+ <>
+
+ Hold ⇧ to drop image
+ >
+ );
+};
diff --git a/gui/src/components/mainInput/tiptap/StyledComponents.ts b/gui/src/components/mainInput/tiptap/StyledComponents.ts
new file mode 100644
index 0000000000..2aca4f1be2
--- /dev/null
+++ b/gui/src/components/mainInput/tiptap/StyledComponents.ts
@@ -0,0 +1,72 @@
+import styled from "styled-components";
+import {
+ defaultBorderRadius,
+ lightGray,
+ vscBadgeBackground,
+ vscCommandCenterActiveBorder,
+ vscCommandCenterInactiveBorder,
+ vscForeground,
+ vscInputBackground,
+ vscInputBorderFocus,
+} from "../..";
+import { getFontSize } from "../../../util";
+
+export const InputBoxDiv = styled.div<{}>`
+ resize: none;
+ font-family: inherit;
+ border-radius: ${defaultBorderRadius};
+ padding-bottom: 1px;
+ margin: 0;
+ height: auto;
+ width: 100%;
+ background-color: ${vscInputBackground};
+ color: ${vscForeground};
+
+ border: 1px solid ${vscCommandCenterInactiveBorder};
+ transition: border-color 0.15s ease-in-out;
+ &:focus-within {
+ border: 1px solid ${vscCommandCenterActiveBorder};
+ }
+
+ outline: none;
+ font-size: ${getFontSize()}px;
+
+ &:focus {
+ outline: none;
+
+ border: 0.5px solid ${vscInputBorderFocus};
+ }
+
+ &::placeholder {
+ color: ${lightGray}cc;
+ }
+
+ display: flex;
+ flex-direction: column;
+`;
+
+export const HoverDiv = styled.div`
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ opacity: 0.5;
+ background-color: ${vscBadgeBackground};
+ color: ${vscForeground};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`;
+
+export const HoverTextDiv = styled.div`
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ color: ${vscForeground};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`;
diff --git a/gui/src/components/mainInput/TipTapEditor.css b/gui/src/components/mainInput/tiptap/TipTapEditor.css
similarity index 100%
rename from gui/src/components/mainInput/TipTapEditor.css
rename to gui/src/components/mainInput/tiptap/TipTapEditor.css
diff --git a/gui/src/components/mainInput/tiptap/TipTapEditor.tsx b/gui/src/components/mainInput/tiptap/TipTapEditor.tsx
new file mode 100644
index 0000000000..722cb007c9
--- /dev/null
+++ b/gui/src/components/mainInput/tiptap/TipTapEditor.tsx
@@ -0,0 +1,283 @@
+import { Editor, EditorContent, JSONContent } from "@tiptap/react";
+import { ContextProviderDescription, InputModifiers } from "core";
+import { modelSupportsImages } from "core/llm/autodetect";
+import { useCallback, useContext, useEffect, useState } from "react";
+import { IdeMessengerContext } from "../../../context/IdeMessenger";
+import useIsOSREnabled from "../../../hooks/useIsOSREnabled";
+import useUpdatingRef from "../../../hooks/useUpdatingRef";
+import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
+import { selectDefaultModel } from "../../../redux/slices/configSlice";
+import {
+ selectIsInEditMode,
+ setMainEditorContentTrigger,
+} from "../../../redux/slices/sessionSlice";
+import InputToolbar, { ToolbarOptions } from "../InputToolbar";
+import { ComboBoxItem } from "../types";
+import { DragOverlay } from "./DragOverlay";
+import { createEditorConfig, getPlaceholderText } from "./editorConfig";
+import { handleImageFile } from "./imageUtils";
+import { useEditorEventHandlers } from "./keyHandlers";
+import { InputBoxDiv } from "./StyledComponents";
+import "./TipTapEditor.css";
+import { useWebviewListeners } from "./useWebviewListeners";
+
+export interface TipTapEditorProps {
+ availableContextProviders: ContextProviderDescription[];
+ availableSlashCommands: ComboBoxItem[];
+ isMainInput: boolean;
+ onEnter: (
+ editorState: JSONContent,
+ modifiers: InputModifiers,
+ editor: Editor,
+ ) => void;
+ editorState?: JSONContent;
+ toolbarOptions?: ToolbarOptions;
+ lumpOpen: boolean;
+ setLumpOpen: (open: boolean) => void;
+ placeholder?: string;
+ historyKey: string;
+ inputId: string;
+}
+
+export const TIPPY_DIV_ID = "tippy-js-div";
+
+function TipTapEditor(props: TipTapEditorProps) {
+ const dispatch = useAppDispatch();
+
+ const ideMessenger = useContext(IdeMessengerContext);
+
+ const isOSREnabled = useIsOSREnabled();
+
+ const defaultModel = useAppSelector(selectDefaultModel);
+ const isStreaming = useAppSelector((state) => state.session.isStreaming);
+ const isInEditMode = useAppSelector(selectIsInEditMode);
+ const historyLength = useAppSelector((store) => store.session.history.length);
+
+ const { editor, onEnterRef } = createEditorConfig({
+ props,
+ ideMessenger,
+ dispatch,
+ });
+
+ const [shouldHideToolbar, setShouldHideToolbar] = useState(true);
+
+ useEffect(() => {
+ if (!editor) {
+ return;
+ }
+ const placeholder = getPlaceholderText(props.placeholder, historyLength);
+
+ editor.extensionManager.extensions.filter(
+ (extension) => extension.name === "placeholder",
+ )[0].options["placeholder"] = placeholder;
+
+ editor.view.dispatch(editor.state.tr);
+ }, [editor, props.placeholder, historyLength]);
+
+ useEffect(() => {
+ if (props.isMainInput) {
+ editor?.commands.clearContent(true);
+ }
+ }, [editor, isInEditMode, props.isMainInput]);
+
+ const editorFocusedRef = useUpdatingRef(editor?.isFocused, [editor]);
+
+ useEffect(() => {
+ if (props.isMainInput) {
+ /**
+ * I have a strong suspicion that many of the other focus
+ * commands are redundant, especially the ones inside
+ * useTimeout.
+ */
+ editor?.commands.focus();
+ }
+ }, [props.isMainInput, editor]);
+
+ // Re-focus main input after done generating
+ useEffect(() => {
+ if (editor && !isStreaming && props.isMainInput && document.hasFocus()) {
+ editor.commands.focus(undefined, { scrollIntoView: false });
+ }
+ }, [props.isMainInput, isStreaming, editor]);
+
+ // This allows anywhere in the app to set the content of the main input
+ const mainInputContentTrigger = useAppSelector(
+ (store) => store.session.mainEditorContentTrigger,
+ );
+ useEffect(() => {
+ if (!props.isMainInput || !mainInputContentTrigger) {
+ return;
+ }
+ queueMicrotask(() => {
+ editor?.commands.setContent(mainInputContentTrigger);
+ });
+ dispatch(setMainEditorContentTrigger(undefined));
+ }, [editor, props.isMainInput, mainInputContentTrigger]);
+
+ // IDE event listeners
+ useWebviewListeners({
+ editor,
+ onEnterRef,
+ dispatch,
+ historyLength,
+ props,
+ editorFocusedRef,
+ });
+
+ const [showDragOverMsg, setShowDragOverMsg] = useState(false);
+
+ const [activeKey, setActiveKey] = useState(null);
+
+ const insertCharacterWithWhitespace = useCallback(
+ (char: string) => {
+ if (!editor) {
+ return;
+ }
+ const text = editor.getText();
+ if (!text.endsWith(char)) {
+ if (text.length > 0 && !text.endsWith(" ")) {
+ editor.commands.insertContent(` ${char}`);
+ } else {
+ editor.commands.insertContent(char);
+ }
+ }
+ },
+ [editor],
+ );
+
+ const { handleKeyUp, handleKeyDown } = useEditorEventHandlers({
+ editor,
+ isOSREnabled: isOSREnabled,
+ editorFocusedRef,
+ isInEditMode,
+ setActiveKey,
+ });
+
+ const handleBlur = (e: React.FocusEvent) => {
+ // Check if the new focus target is within our InputBoxDiv
+ const currentTarget = e.currentTarget;
+ const relatedTarget = e.relatedTarget as Node | null;
+
+ if (relatedTarget && currentTarget.contains(relatedTarget)) {
+ return;
+ }
+
+ setShouldHideToolbar(true);
+ };
+
+ return (
+ {
+ setShouldHideToolbar(false);
+ }}
+ onBlur={handleBlur}
+ onKeyDown={handleKeyDown}
+ onKeyUp={handleKeyUp}
+ className={shouldHideToolbar ? "cursor-default" : "cursor-text"}
+ onClick={() => {
+ editor?.commands.focus();
+ }}
+ onDragOver={(event) => {
+ event.preventDefault();
+ setShowDragOverMsg(true);
+ }}
+ onDragLeave={(e) => {
+ if (e.relatedTarget === null) {
+ if (e.shiftKey) {
+ setShowDragOverMsg(false);
+ } else {
+ setTimeout(() => setShowDragOverMsg(false), 2000);
+ }
+ }
+ }}
+ onDragEnter={() => {
+ setShowDragOverMsg(true);
+ }}
+ onDrop={(event) => {
+ if (
+ !defaultModel ||
+ !modelSupportsImages(
+ defaultModel.provider,
+ defaultModel.model,
+ defaultModel.title,
+ defaultModel.capabilities,
+ )
+ ) {
+ return;
+ }
+ setShowDragOverMsg(false);
+ let file = event.dataTransfer.files[0];
+ handleImageFile(ideMessenger, file).then((result) => {
+ if (!editor) {
+ return;
+ }
+ if (result) {
+ const [_, dataUrl] = result;
+ const { schema } = editor.state;
+ const node = schema.nodes.image.create({ src: dataUrl });
+ const tr = editor.state.tr.insert(0, node);
+ editor.view.dispatch(tr);
+ }
+ });
+ event.preventDefault();
+ }}
+ >
+
+ {/* */}
+ {
+ event.stopPropagation();
+ }}
+ />
+ {(shouldHideToolbar && !props.isMainInput) || (
+ insertCharacterWithWhitespace("@")}
+ lumpOpen={props.lumpOpen}
+ setLumpOpen={props.setLumpOpen}
+ onEnter={onEnterRef.current}
+ onImageFileSelected={(file) => {
+ handleImageFile(ideMessenger, file).then((result) => {
+ if (!editor) {
+ return;
+ }
+ if (result) {
+ const [_, dataUrl] = result;
+ const { schema } = editor.state;
+ const node = schema.nodes.image.create({ src: dataUrl });
+ editor.commands.command(({ tr }) => {
+ tr.insert(0, node);
+ return true;
+ });
+ }
+ });
+ }}
+ disabled={isStreaming}
+ />
+ )}
+
+
+ {showDragOverMsg &&
+ modelSupportsImages(
+ defaultModel?.provider || "",
+ defaultModel?.model || "",
+ defaultModel?.title,
+ defaultModel?.capabilities,
+ ) && (
+
+ )}
+
+
+ );
+}
+
+export default TipTapEditor;
diff --git a/gui/src/components/mainInput/tiptap/editorConfig.ts b/gui/src/components/mainInput/tiptap/editorConfig.ts
new file mode 100644
index 0000000000..d79981d398
--- /dev/null
+++ b/gui/src/components/mainInput/tiptap/editorConfig.ts
@@ -0,0 +1,409 @@
+import { Editor } from "@tiptap/core";
+import Document from "@tiptap/extension-document";
+import History from "@tiptap/extension-history";
+import Image from "@tiptap/extension-image";
+import Paragraph from "@tiptap/extension-paragraph";
+import Placeholder from "@tiptap/extension-placeholder";
+import Text from "@tiptap/extension-text";
+import { Plugin } from "@tiptap/pm/state";
+import { useEditor } from "@tiptap/react";
+import { InputModifiers } from "core";
+import { modelSupportsImages } from "core/llm/autodetect";
+import { usePostHog } from "posthog-js/react";
+import { useRef } from "react";
+import { IIdeMessenger } from "../../../context/IdeMessenger";
+import { useSubmenuContextProviders } from "../../../context/SubmenuContextProviders";
+import { useInputHistory } from "../../../hooks/useInputHistory";
+import useUpdatingRef from "../../../hooks/useUpdatingRef";
+import { useAppSelector } from "../../../redux/hooks";
+import { selectUseActiveFile } from "../../../redux/selectors";
+import { selectDefaultModel } from "../../../redux/slices/configSlice";
+import {
+ addCodeToEdit,
+ selectHasCodeToEdit,
+ selectIsInEditMode,
+} from "../../../redux/slices/sessionSlice";
+import { AppDispatch } from "../../../redux/store";
+import { exitEditMode } from "../../../redux/thunks";
+import { loadLastSession } from "../../../redux/thunks/session";
+import { getFontSize } from "../../../util";
+import { AddCodeToEdit } from "./extensions/AddCodeToEditExtension";
+import { CodeBlockExtension } from "./extensions/CodeBlockExtension";
+import { SlashCommand } from "./extensions/CommandsExtension";
+import { MockExtension } from "./extensions/FillerExtension";
+import { Mention } from "./extensions/MentionExtension";
+import {
+ getContextProviderDropdownOptions,
+ getSlashCommandDropdownOptions,
+} from "./getSuggestion";
+import { handleImageFile } from "./imageUtils";
+import { TipTapEditorProps } from "./TipTapEditor";
+
+export function getPlaceholderText(
+ placeholder: TipTapEditorProps["placeholder"],
+ historyLength: number,
+) {
+ if (placeholder) {
+ return placeholder;
+ }
+
+ return historyLength === 0
+ ? "Ask anything, '/' for prompts, '@' to add context"
+ : "Ask a follow-up";
+}
+
+/**
+ * This function is called only once, so we need to use refs to pass in the latest values
+ */
+export function createEditorConfig(options: {
+ props: TipTapEditorProps;
+ ideMessenger: IIdeMessenger;
+ dispatch: AppDispatch;
+}) {
+ const { props, ideMessenger, dispatch } = options;
+
+ const posthog = usePostHog();
+
+ // #region Selectors
+ const { getSubmenuContextItems } = useSubmenuContextProviders();
+ const defaultModel = useAppSelector(selectDefaultModel);
+ const isStreaming = useAppSelector((state) => state.session.isStreaming);
+ const useActiveFile = useAppSelector(selectUseActiveFile);
+ const historyLength = useAppSelector((store) => store.session.history.length);
+ const isInEditMode = useAppSelector(selectIsInEditMode);
+ const hasCodeToEdit = useAppSelector(selectHasCodeToEdit);
+ const isEditModeAndNoCodeToEdit = isInEditMode && !hasCodeToEdit;
+ // #endregion
+
+ // #region Refs
+ const inSubmenuRef = useRef(undefined);
+ const inDropdownRef = useRef(false);
+ const defaultModelRef = useUpdatingRef(defaultModel);
+ const isStreamingRef = useUpdatingRef(isStreaming);
+ const isInEditModeRef = useUpdatingRef(isInEditMode);
+ const getSubmenuContextItemsRef = useUpdatingRef(getSubmenuContextItems);
+ const availableContextProvidersRef = useUpdatingRef(
+ props.availableContextProviders,
+ );
+ const historyLengthRef = useUpdatingRef(historyLength);
+ const availableSlashCommandsRef = useUpdatingRef(
+ props.availableSlashCommands,
+ );
+ const { prevRef, nextRef, addRef } = useInputHistory(props.historyKey);
+
+ // #endregion
+
+ const enterSubmenu = async (editor: Editor, providerId: string) => {
+ const contents = editor.getText();
+ const indexOfAt = contents.lastIndexOf("@");
+ if (indexOfAt === -1) {
+ return;
+ }
+
+ // Find the position of the last @ character
+ // We do this because editor.getText() isn't a correct representation including node views
+ let startPos = editor.state.selection.anchor;
+ while (
+ startPos > 0 &&
+ editor.state.doc.textBetween(startPos, startPos + 1) !== "@"
+ ) {
+ startPos--;
+ }
+ startPos++;
+
+ editor.commands.deleteRange({
+ from: startPos,
+ to: editor.state.selection.anchor,
+ });
+ inSubmenuRef.current = providerId;
+
+ // to trigger refresh of suggestions
+ editor.commands.insertContent(":");
+ editor.commands.deleteRange({
+ from: editor.state.selection.anchor - 1,
+ to: editor.state.selection.anchor,
+ });
+ };
+
+ const onClose = () => {
+ inSubmenuRef.current = undefined;
+ inDropdownRef.current = false;
+ };
+
+ const onOpen = () => {
+ inDropdownRef.current = true;
+ };
+
+ const editor: Editor | null = useEditor({
+ extensions: [
+ Document,
+ History,
+ Image.extend({
+ addProseMirrorPlugins() {
+ const plugin = new Plugin({
+ props: {
+ handleDOMEvents: {
+ paste(view, event) {
+ const model = defaultModelRef.current;
+ if (!model) return;
+ const items = event.clipboardData?.items;
+ if (items) {
+ for (const item of items) {
+ const file = item.getAsFile();
+ file &&
+ modelSupportsImages(
+ model.provider,
+ model.model,
+ model.title,
+ model.capabilities,
+ ) &&
+ handleImageFile(ideMessenger, file).then((resp) => {
+ if (!resp) return;
+ const [img, dataUrl] = resp;
+ const { schema } = view.state;
+ const node = schema.nodes.image.create({
+ src: dataUrl,
+ });
+ const tr = view.state.tr.insert(0, node);
+ view.dispatch(tr);
+ });
+ }
+ }
+ },
+ },
+ },
+ });
+ return [plugin];
+ },
+ }).configure({
+ HTMLAttributes: {
+ class: "object-contain max-h-[210px] max-w-full mx-1",
+ },
+ }),
+ Placeholder.configure({
+ placeholder: getPlaceholderText(
+ props.placeholder,
+ historyLengthRef.current,
+ ),
+ }),
+ Paragraph.extend({
+ addKeyboardShortcuts() {
+ return {
+ Enter: () => {
+ if (inDropdownRef.current) {
+ return false;
+ }
+
+ onEnterRef.current({
+ useCodebase: false,
+ noContext: !useActiveFile,
+ });
+ return true;
+ },
+
+ "Mod-Enter": () => {
+ onEnterRef.current({
+ useCodebase: true,
+ noContext: !useActiveFile,
+ });
+ return true;
+ },
+ "Alt-Enter": () => {
+ posthog.capture("gui_use_active_file_enter");
+
+ onEnterRef.current({
+ useCodebase: false,
+ noContext: !!useActiveFile,
+ });
+
+ return true;
+ },
+ "Mod-Backspace": () => {
+ // If you press cmd+backspace wanting to cancel,
+ // but are inside of a text box, it shouldn't
+ // delete the text
+ if (isStreamingRef.current) {
+ return true;
+ }
+ return false;
+ },
+ "Shift-Enter": () =>
+ this.editor.commands.first(({ commands }) => [
+ () => commands.newlineInCode(),
+ () => commands.createParagraphNear(),
+ () => commands.liftEmptyBlock(),
+ () => commands.splitBlock(),
+ ]),
+
+ ArrowUp: () => {
+ if (this.editor.state.selection.anchor > 1) {
+ return false;
+ }
+
+ const previousInput = prevRef.current(
+ this.editor.state.toJSON().doc,
+ );
+ if (previousInput) {
+ this.editor.commands.setContent(previousInput);
+ setTimeout(() => {
+ this.editor.commands.blur();
+ this.editor.commands.focus("start");
+ }, 0);
+ return true;
+ }
+ return false;
+ },
+ Escape: () => {
+ if (inDropdownRef.current || !isInEditModeRef.current) {
+ ideMessenger.post("focusEditor", undefined);
+ return true;
+ }
+ (async () => {
+ await dispatch(
+ loadLastSession({
+ saveCurrentSession: false,
+ }),
+ );
+ dispatch(exitEditMode());
+ })();
+
+ return true;
+ },
+ ArrowDown: () => {
+ if (
+ this.editor.state.selection.anchor <
+ this.editor.state.doc.content.size - 1
+ ) {
+ return false;
+ }
+ const nextInput = nextRef.current();
+ if (nextInput) {
+ this.editor.commands.setContent(nextInput);
+ setTimeout(() => {
+ this.editor.commands.blur();
+ this.editor.commands.focus("end");
+ }, 0);
+ return true;
+ }
+ return false;
+ },
+ };
+ },
+ }).configure({
+ HTMLAttributes: {
+ class: "my-1",
+ },
+ }),
+ Text,
+ Mention.configure({
+ HTMLAttributes: {
+ class: "mention",
+ },
+ suggestion: getContextProviderDropdownOptions(
+ availableContextProvidersRef,
+ getSubmenuContextItemsRef,
+ enterSubmenu,
+ onClose,
+ onOpen,
+ inSubmenuRef,
+ ideMessenger,
+ ),
+ renderHTML: (props) => {
+ return `@${props.node.attrs.label || props.node.attrs.id}`;
+ },
+ }),
+
+ AddCodeToEdit.configure({
+ HTMLAttributes: {
+ class: "add-code-to-edit",
+ },
+ suggestion: {
+ ...getContextProviderDropdownOptions(
+ availableContextProvidersRef,
+ getSubmenuContextItemsRef,
+ enterSubmenu,
+ onClose,
+ onOpen,
+ inSubmenuRef,
+ ideMessenger,
+ ),
+ allow: () => isInEditModeRef.current,
+ command: async ({ editor, range, props }) => {
+ editor.chain().focus().insertContentAt(range, "").run();
+ const filepath = props.id;
+ const contents = await ideMessenger.ide.readFile(filepath);
+ dispatch(
+ addCodeToEdit({
+ filepath,
+ contents,
+ }),
+ );
+ },
+ items: async ({ query }) => {
+ // Only display files in the dropdown
+ const results = getSubmenuContextItemsRef.current("file", query);
+ return results.map((result) => ({
+ ...result,
+ label: result.title,
+ type: "file",
+ query: result.id,
+ icon: result.icon,
+ }));
+ },
+ },
+ }),
+ props.availableSlashCommands.length
+ ? SlashCommand.configure({
+ HTMLAttributes: {
+ class: "mention",
+ },
+ suggestion: getSlashCommandDropdownOptions(
+ availableSlashCommandsRef,
+ onClose,
+ onOpen,
+ ideMessenger,
+ ),
+ renderText: (props) => {
+ return props.node.attrs.label;
+ },
+ })
+ : MockExtension,
+ CodeBlockExtension,
+ ],
+ editorProps: {
+ attributes: {
+ class: "outline-none -mt-1 overflow-hidden",
+ style: `font-size: ${getFontSize()}px;`,
+ },
+ },
+ content: props.editorState,
+ editable: !isStreaming || props.isMainInput,
+ });
+
+ const onEnterRef = useUpdatingRef(
+ (modifiers: InputModifiers) => {
+ if (!editor) {
+ return;
+ }
+ if (isStreaming || isEditModeAndNoCodeToEdit) {
+ return;
+ }
+
+ const json = editor.getJSON();
+
+ // Don't do anything if input box is empty
+ if (!json.content?.some((c) => c.content)) {
+ return;
+ }
+
+ if (props.isMainInput) {
+ addRef.current(json);
+ }
+
+ props.onEnter(json, modifiers, editor);
+ },
+ [props.onEnter, editor, props.isMainInput],
+ );
+
+ return { editor, onEnterRef };
+}
diff --git a/gui/src/components/mainInput/AddCodeToEditExtension.ts b/gui/src/components/mainInput/tiptap/extensions/AddCodeToEditExtension.ts
similarity index 100%
rename from gui/src/components/mainInput/AddCodeToEditExtension.ts
rename to gui/src/components/mainInput/tiptap/extensions/AddCodeToEditExtension.ts
diff --git a/gui/src/components/mainInput/CodeBlockExtension.tsx b/gui/src/components/mainInput/tiptap/extensions/CodeBlockExtension.tsx
similarity index 91%
rename from gui/src/components/mainInput/CodeBlockExtension.tsx
rename to gui/src/components/mainInput/tiptap/extensions/CodeBlockExtension.tsx
index 091694db91..aea6e18af0 100644
--- a/gui/src/components/mainInput/CodeBlockExtension.tsx
+++ b/gui/src/components/mainInput/tiptap/extensions/CodeBlockExtension.tsx
@@ -1,6 +1,6 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
-import { CodeBlockComponent } from "./CodeBlockComponent";
+import { CodeBlockComponent } from "../CodeBlockComponent";
export const CodeBlockExtension = Node.create({
name: "codeBlock",
diff --git a/gui/src/components/mainInput/CommandsExtension.ts b/gui/src/components/mainInput/tiptap/extensions/CommandsExtension.ts
similarity index 100%
rename from gui/src/components/mainInput/CommandsExtension.ts
rename to gui/src/components/mainInput/tiptap/extensions/CommandsExtension.ts
diff --git a/gui/src/components/mainInput/FillerExtension.tsx b/gui/src/components/mainInput/tiptap/extensions/FillerExtension.tsx
similarity index 100%
rename from gui/src/components/mainInput/FillerExtension.tsx
rename to gui/src/components/mainInput/tiptap/extensions/FillerExtension.tsx
diff --git a/gui/src/components/mainInput/MentionExtension.ts b/gui/src/components/mainInput/tiptap/extensions/MentionExtension.ts
similarity index 100%
rename from gui/src/components/mainInput/MentionExtension.ts
rename to gui/src/components/mainInput/tiptap/extensions/MentionExtension.ts
diff --git a/gui/src/components/mainInput/getSuggestion.ts b/gui/src/components/mainInput/tiptap/getSuggestion.ts
similarity index 97%
rename from gui/src/components/mainInput/getSuggestion.ts
rename to gui/src/components/mainInput/tiptap/getSuggestion.ts
index ab107f1463..27963aab9f 100644
--- a/gui/src/components/mainInput/getSuggestion.ts
+++ b/gui/src/components/mainInput/tiptap/getSuggestion.ts
@@ -6,10 +6,10 @@ import {
} from "core";
import { MutableRefObject } from "react";
import tippy from "tippy.js";
-import { IIdeMessenger } from "../../context/IdeMessenger";
-import MentionList from "./MentionList";
+import { IIdeMessenger } from "../../../context/IdeMessenger";
+import AtMentionDropdown from "../AtMentionDropdown";
+import { ComboBoxItem, ComboBoxItemType, ComboBoxSubAction } from "../types";
import { TIPPY_DIV_ID } from "./TipTapEditor";
-import { ComboBoxItem, ComboBoxItemType, ComboBoxSubAction } from "./types";
function getSuggestion(
items: (props: { query: string }) => Promise,
@@ -32,7 +32,7 @@ function getSuggestion(
return {
onStart: (props: any) => {
- component = new ReactRenderer(MentionList, {
+ component = new ReactRenderer(AtMentionDropdown, {
props: { ...props, enterSubmenu, onClose: onExit },
editor: props.editor,
});
diff --git a/gui/src/components/mainInput/tiptap/imageUtils.ts b/gui/src/components/mainInput/tiptap/imageUtils.ts
new file mode 100644
index 0000000000..0803e5ab8b
--- /dev/null
+++ b/gui/src/components/mainInput/tiptap/imageUtils.ts
@@ -0,0 +1,73 @@
+import { IIdeMessenger } from "../../../context/IdeMessenger";
+
+const IMAGE_RESOLUTION = 1024;
+
+export function getDataUrlForFile(
+ file: File,
+ img: HTMLImageElement,
+): string | undefined {
+ const targetWidth = IMAGE_RESOLUTION;
+ const targetHeight = IMAGE_RESOLUTION;
+ const scaleFactor = Math.min(
+ targetWidth / img.width,
+ targetHeight / img.height,
+ );
+
+ const canvas = document.createElement("canvas");
+ canvas.width = img.width * scaleFactor;
+ canvas.height = img.height * scaleFactor;
+
+ const ctx = canvas.getContext("2d");
+ if (!ctx) {
+ console.error("Error getting image data url: 2d context not found");
+ return;
+ }
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
+
+ const downsizedDataUrl = canvas.toDataURL("image/jpeg", 0.7);
+ return downsizedDataUrl;
+}
+
+export async function handleImageFile(
+ ideMessenger: IIdeMessenger,
+ file: File,
+): Promise<[HTMLImageElement, string] | undefined> {
+ let filesize = file.size / 1024 / 1024; // filesize in MB
+ // check image type and size
+ if (
+ [
+ "image/jpeg",
+ "image/jpg",
+ "image/png",
+ "image/gif",
+ "image/svg",
+ "image/webp",
+ ].includes(file.type) &&
+ filesize < 10
+ ) {
+ // check dimensions
+ let _URL = window.URL || window.webkitURL;
+ let img = new window.Image();
+ img.src = _URL.createObjectURL(file);
+
+ return await new Promise((resolve) => {
+ img.onload = function () {
+ const dataUrl = getDataUrlForFile(file, img);
+ if (!dataUrl) {
+ return;
+ }
+
+ let image = new window.Image();
+ image.src = dataUrl;
+ image.onload = function () {
+ resolve([image, dataUrl]);
+ };
+ };
+ });
+ } else {
+ ideMessenger.post("showToast", [
+ "error",
+ "Images need to be in jpg or png format and less than 10MB in size.",
+ ]);
+ }
+}
diff --git a/gui/src/components/mainInput/tiptap/keyHandlers.ts b/gui/src/components/mainInput/tiptap/keyHandlers.ts
new file mode 100644
index 0000000000..c25a53271f
--- /dev/null
+++ b/gui/src/components/mainInput/tiptap/keyHandlers.ts
@@ -0,0 +1,50 @@
+import { Editor } from "@tiptap/react";
+import { KeyboardEvent } from "react";
+import { isJetBrains, isMetaEquivalentKeyPressed } from "../../../util";
+import {
+ handleJetBrainsOSRMetaKeyIssues,
+ handleVSCMetaKeyIssues,
+} from "../util/handleMetaKeyIssues";
+
+export function useEditorEventHandlers(options: {
+ editor: Editor | null;
+ isOSREnabled: boolean;
+ editorFocusedRef: React.MutableRefObject;
+ isInEditMode: boolean;
+ setActiveKey: (key: string | null) => void;
+}) {
+ const { editor, isOSREnabled, editorFocusedRef, isInEditMode, setActiveKey } =
+ options;
+
+ /**
+ * This handles various issues with meta key actions
+ * - In JetBrains, when using OSR in JCEF, there is a bug where using the meta key to
+ * highlight code using arrow keys is not working
+ * - In VS Code, while working with .ipynb files there is a problem where copy/paste/cut will affect
+ * the actual notebook cells, even when performing them in our GUI
+ *
+ * Currently keydown events for a number of keys are not registering if the
+ * meta/shift key is pressed, for example "x", "c", "v", "z", etc.
+ * Until this is resolved we can't turn on OSR for non-Mac users due to issues
+ * with those key actions.
+ */
+ const handleKeyDown = async (e: KeyboardEvent) => {
+ if (!editor) {
+ return;
+ }
+
+ setActiveKey(e.key);
+
+ if (!editorFocusedRef?.current || !isMetaEquivalentKeyPressed(e)) return;
+
+ if (isOSREnabled) {
+ handleJetBrainsOSRMetaKeyIssues(e, editor);
+ } else if (!isJetBrains()) {
+ await handleVSCMetaKeyIssues(e, editor);
+ }
+ };
+
+ const handleKeyUp = () => setActiveKey(null);
+
+ return { handleKeyDown, handleKeyUp };
+}
diff --git a/gui/src/components/mainInput/resolveInput.ts b/gui/src/components/mainInput/tiptap/resolveInput.ts
similarity index 98%
rename from gui/src/components/mainInput/resolveInput.ts
rename to gui/src/components/mainInput/tiptap/resolveInput.ts
index 1113d2fc38..d9eb47b6c7 100644
--- a/gui/src/components/mainInput/resolveInput.ts
+++ b/gui/src/components/mainInput/tiptap/resolveInput.ts
@@ -1,3 +1,4 @@
+import { Dispatch } from "@reduxjs/toolkit";
import { JSONContent } from "@tiptap/react";
import {
ContextItemWithId,
@@ -8,12 +9,11 @@ import {
RangeInFile,
TextMessagePart,
} from "core";
-import { stripImages } from "core/util/messageContent";
-import { IIdeMessenger } from "../../context/IdeMessenger";
-import { Dispatch } from "@reduxjs/toolkit";
-import { setIsGatheringContext } from "../../redux/slices/sessionSlice";
import { ctxItemToRifWithContents } from "core/commands/util";
+import { stripImages } from "core/util/messageContent";
import { getUriFileExtension } from "core/util/uri";
+import { IIdeMessenger } from "../../../context/IdeMessenger";
+import { setIsGatheringContext } from "../../../redux/slices/sessionSlice";
interface MentionAttrs {
label: string;
diff --git a/gui/src/components/mainInput/tiptap/useWebviewListeners.ts b/gui/src/components/mainInput/tiptap/useWebviewListeners.ts
new file mode 100644
index 0000000000..3a46949395
--- /dev/null
+++ b/gui/src/components/mainInput/tiptap/useWebviewListeners.ts
@@ -0,0 +1,211 @@
+import { Editor } from "@tiptap/core";
+import { InputModifiers } from "core";
+import { rifWithContentsToContextItem } from "core/commands/util";
+import { MutableRefObject } from "react";
+import { useWebviewListener } from "../../../hooks/useWebviewListener";
+import {
+ clearCodeToEdit,
+ setNewestCodeblocksForInput,
+} from "../../../redux/slices/sessionSlice";
+import { AppDispatch } from "../../../redux/store";
+import { loadSession, saveCurrentSession } from "../../../redux/thunks/session";
+import { TipTapEditorProps } from "./TipTapEditor";
+
+export function useWebviewListeners(options: {
+ editor: Editor | null;
+ onEnterRef: MutableRefObject<(modifiers: InputModifiers) => void>;
+ dispatch: AppDispatch;
+ historyLength: number;
+ props: TipTapEditorProps;
+ editorFocusedRef: MutableRefObject;
+}) {
+ const {
+ editor,
+ onEnterRef,
+ dispatch,
+ historyLength,
+ props,
+ editorFocusedRef,
+ } = options;
+
+ useWebviewListener(
+ "userInput",
+ async (data) => {
+ if (!props.isMainInput) {
+ return;
+ }
+ editor?.commands.insertContent(data.input);
+ onEnterRef.current({ useCodebase: false, noContext: true });
+ },
+ [editor, onEnterRef.current, props.isMainInput],
+ );
+
+ useWebviewListener("jetbrains/editorInsetRefresh", async () => {
+ editor?.chain().clearContent().focus().run();
+ });
+
+ useWebviewListener(
+ "focusContinueInput",
+ async (data) => {
+ if (!props.isMainInput) {
+ return;
+ }
+
+ dispatch(clearCodeToEdit());
+
+ if (historyLength > 0) {
+ await dispatch(
+ saveCurrentSession({
+ openNewSession: false,
+ generateTitle: true,
+ }),
+ );
+ }
+ setTimeout(() => {
+ editor?.commands.blur();
+ editor?.commands.focus("end");
+ }, 20);
+ },
+ [historyLength, editor, props.isMainInput],
+ );
+
+ useWebviewListener(
+ "focusContinueInputWithoutClear",
+ async () => {
+ if (!props.isMainInput) {
+ return;
+ }
+ setTimeout(() => {
+ editor?.commands.focus("end");
+ }, 20);
+ },
+ [editor, props.isMainInput],
+ );
+
+ useWebviewListener(
+ "focusContinueInputWithNewSession",
+ async () => {
+ if (!props.isMainInput) {
+ return;
+ }
+ await dispatch(
+ saveCurrentSession({
+ openNewSession: true,
+ generateTitle: true,
+ }),
+ );
+ setTimeout(() => {
+ editor?.commands.focus("end");
+ }, 20);
+ },
+ [editor, props.isMainInput],
+ );
+
+ useWebviewListener(
+ "highlightedCode",
+ async (data) => {
+ if (!props.isMainInput || !editor) {
+ return;
+ }
+
+ const contextItem = rifWithContentsToContextItem(
+ data.rangeInFileWithContents,
+ );
+
+ let index = 0;
+ for (const el of editor.getJSON()?.content ?? []) {
+ if (el.attrs?.item?.name === contextItem.name) {
+ return; // Prevent exact duplicate code blocks
+ }
+ if (el.type === "codeBlock") {
+ index += 2;
+ } else {
+ break;
+ }
+ }
+ editor
+ .chain()
+ .insertContentAt(index, {
+ type: "codeBlock",
+ attrs: {
+ item: contextItem,
+ inputId: props.inputId,
+ },
+ })
+ .run();
+ dispatch(
+ setNewestCodeblocksForInput({
+ inputId: props.inputId,
+ contextItemId: contextItem.id.itemId,
+ }),
+ );
+ if (data.prompt) {
+ editor.commands.focus("end");
+ editor.commands.insertContent(data.prompt);
+ }
+
+ if (data.shouldRun) {
+ onEnterRef.current({ useCodebase: false, noContext: true });
+ }
+
+ setTimeout(() => {
+ editor.commands.blur();
+ editor.commands.focus("end");
+ }, 20);
+ },
+ [editor, props.isMainInput, historyLength, onEnterRef.current],
+ );
+
+ useWebviewListener(
+ "focusEdit",
+ async () => {
+ if (!props.isMainInput) {
+ return;
+ }
+
+ setTimeout(() => {
+ editor?.commands.focus("end");
+ }, 20);
+ },
+ [editor, props.isMainInput],
+ );
+
+ useWebviewListener(
+ "focusEditWithoutClear",
+ async () => {
+ if (!props.isMainInput) {
+ return;
+ }
+
+ setTimeout(() => {
+ editor?.commands.focus("end");
+ }, 2000);
+ },
+ [editor, props.isMainInput],
+ );
+
+ useWebviewListener(
+ "isContinueInputFocused",
+ async () => {
+ return props.isMainInput && !!editorFocusedRef.current;
+ },
+ [editorFocusedRef, props.isMainInput],
+ !props.isMainInput,
+ );
+
+ useWebviewListener(
+ "focusContinueSessionId",
+ async (data) => {
+ if (!props.isMainInput || !data.sessionId) {
+ return;
+ }
+ await dispatch(
+ loadSession({
+ sessionId: data.sessionId,
+ saveCurrentSession: true,
+ }),
+ );
+ },
+ [props.isMainInput],
+ );
+}
diff --git a/gui/src/components/mainInput/handleMetaKeyIssues.ts b/gui/src/components/mainInput/util/handleMetaKeyIssues.ts
similarity index 98%
rename from gui/src/components/mainInput/handleMetaKeyIssues.ts
rename to gui/src/components/mainInput/util/handleMetaKeyIssues.ts
index 1adf5593a8..9f9078303e 100644
--- a/gui/src/components/mainInput/handleMetaKeyIssues.ts
+++ b/gui/src/components/mainInput/util/handleMetaKeyIssues.ts
@@ -1,6 +1,6 @@
import { Editor } from "@tiptap/react";
import { KeyboardEvent } from "react";
-import { getPlatform, isWebEnvironment } from "../../util";
+import { getPlatform, isWebEnvironment } from "../../../util";
const isWebEnv = isWebEnvironment();
diff --git a/gui/src/components/mainInput/inputModifiers.ts b/gui/src/components/mainInput/util/inputModifiers.ts
similarity index 100%
rename from gui/src/components/mainInput/inputModifiers.ts
rename to gui/src/components/mainInput/util/inputModifiers.ts
diff --git a/gui/src/components/markdown/StepContainerPreActionButtons.tsx b/gui/src/components/markdown/StepContainerPreActionButtons.tsx
index 5cb33d9397..85ece0990f 100644
--- a/gui/src/components/markdown/StepContainerPreActionButtons.tsx
+++ b/gui/src/components/markdown/StepContainerPreActionButtons.tsx
@@ -1,22 +1,26 @@
-import { useContext, useRef, useState } from "react";
import {
+ ArrowLeftEndOnRectangleIcon,
CommandLineIcon,
PlayIcon,
- ArrowLeftEndOnRectangleIcon,
} from "@heroicons/react/24/outline";
-import { defaultBorderRadius, vscEditorBackground } from "..";
-import { IdeMessengerContext } from "../../context/IdeMessenger";
-import { isJetBrains } from "../../util";
-import { isTerminalCodeBlock, getTerminalCommand } from "./utils";
-import HeaderButtonWithToolTip from "../gui/HeaderButtonWithToolTip";
-import { CopyIconButton } from "../gui/CopyIconButton";
+import { useContext, useRef, useState } from "react";
import { v4 as uuidv4 } from "uuid";
+import {
+ defaultBorderRadius,
+ vscCommandCenterInactiveBorder,
+ vscEditorBackground,
+} from "..";
+import { IdeMessengerContext } from "../../context/IdeMessenger";
import { useWebviewListener } from "../../hooks/useWebviewListener";
import { useAppSelector } from "../../redux/hooks";
import {
selectDefaultModel,
selectUIConfig,
} from "../../redux/slices/configSlice";
+import { isJetBrains } from "../../util";
+import { CopyIconButton } from "../gui/CopyIconButton";
+import HeaderButtonWithToolTip from "../gui/HeaderButtonWithToolTip";
+import { getTerminalCommand, isTerminalCodeBlock } from "./utils";
interface StepContainerPreActionButtonsProps {
language: string | null;
@@ -84,14 +88,20 @@ export default function StepContainerPreActionButtons({
tabIndex={-1}
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
- className="bg-vsc-editor-background border-vsc-input-border relative my-2.5 rounded-md border-[1px] border-solid"
+ className="bg-vsc-editor-background relative my-2.5"
+ style={{
+ border: `1px solid ${vscCommandCenterInactiveBorder}`,
+ borderRadius: defaultBorderRadius,
+ }}
>
{children}
{hovering && !isStreaming && (
{shouldRunTerminalCmd && (
diff --git a/gui/src/components/markdown/StepContainerPreToolbar/StepContainerPreToolbar.tsx b/gui/src/components/markdown/StepContainerPreToolbar/StepContainerPreToolbar.tsx
index 99e449cc7f..46fa90d683 100644
--- a/gui/src/components/markdown/StepContainerPreToolbar/StepContainerPreToolbar.tsx
+++ b/gui/src/components/markdown/StepContainerPreToolbar/StepContainerPreToolbar.tsx
@@ -4,7 +4,11 @@ import { debounce } from "lodash";
import { useContext, useEffect, useRef, useState } from "react";
import styled from "styled-components";
import { v4 as uuidv4 } from "uuid";
-import { lightGray, vscEditorBackground } from "../..";
+import {
+ defaultBorderRadius,
+ vscCommandCenterInactiveBorder,
+ vscEditorBackground,
+} from "../..";
import { IdeMessengerContext } from "../../../context/IdeMessenger";
import { useWebviewListener } from "../../../hooks/useWebviewListener";
import { useAppSelector } from "../../../redux/hooks";
@@ -22,9 +26,9 @@ import GeneratingCodeLoader from "./GeneratingCodeLoader";
import RunInTerminalButton from "./RunInTerminalButton";
const TopDiv = styled.div`
- outline: 0.5px solid rgba(153, 153, 152);
+ outline: 1px solid ${vscCommandCenterInactiveBorder};
outline-offset: -0.5px;
- border-radius: 2.5px;
+ border-radius: ${defaultBorderRadius};
margin-bottom: 8px !important;
background-color: ${vscEditorBackground};
min-width: 0;
@@ -39,7 +43,7 @@ const ToolbarDiv = styled.div<{ isExpanded: boolean }>`
padding: 4px 6px;
margin: 0;
border-bottom: ${({ isExpanded }) =>
- isExpanded ? `0.5px solid ${lightGray}80` : "inherit"};
+ isExpanded ? `1px solid ${vscCommandCenterInactiveBorder}` : "inherit"};
`;
export interface StepContainerPreToolbarProps {
@@ -225,7 +229,7 @@ export default function StepContainerPreToolbar(
{isExpanded && (
{props.children}
diff --git a/gui/src/components/modelSelection/ModeSelect.tsx b/gui/src/components/modelSelection/ModeSelect.tsx
new file mode 100644
index 0000000000..afc0588fa2
--- /dev/null
+++ b/gui/src/components/modelSelection/ModeSelect.tsx
@@ -0,0 +1,183 @@
+// A dropdown menu for selecting between Chat, Edit, and Agent modes with keyboard shortcuts
+import { Listbox } from "@headlessui/react";
+import {
+ ChatBubbleLeftIcon,
+ CheckIcon,
+ ChevronDownIcon,
+ PencilIcon,
+ SparklesIcon,
+} from "@heroicons/react/24/outline";
+import { MessageModes } from "core";
+import { modelSupportsTools } from "core/llm/autodetect";
+import { useEffect } from "react";
+import styled from "styled-components";
+import { defaultBorderRadius, lightGray, vscInputBackground } from "..";
+import { useAppDispatch, useAppSelector } from "../../redux/hooks";
+import { selectDefaultModel } from "../../redux/slices/configSlice";
+import {
+ cycleMode,
+ selectCurrentMode,
+ setMode,
+} from "../../redux/slices/sessionSlice";
+import {
+ fontSize,
+ getFontSize,
+ getMetaKeyLabel,
+ isJetBrains,
+} from "../../util";
+
+const StyledListboxButton = styled(Listbox.Button)`
+ font-family: inherit;
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ border: none;
+ cursor: pointer;
+ font-size: ${getFontSize() - 2}px;
+ background: transparent;
+ color: ${lightGray};
+ &:focus {
+ outline: none;
+ }
+`;
+
+const StyledListboxOptions = styled(Listbox.Options)`
+ margin-top: 4px;
+ position: absolute;
+ list-style: none;
+ padding: 0px;
+ min-width: 180px;
+ cursor: default;
+ display: flex;
+ flex-direction: column;
+ border-radius: ${defaultBorderRadius};
+ border: 0.5px solid ${lightGray};
+ background-color: ${vscInputBackground};
+`;
+
+const StyledListboxOption = styled(Listbox.Option)`
+ border-radius: ${defaultBorderRadius};
+ padding: 6px 12px;
+ cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ opacity: ${(props) => (props.disabled ? 0.5 : 1)};
+
+ &:hover {
+ background: ${(props) =>
+ props.disabled ? "transparent" : `${lightGray}33`};
+ }
+`;
+
+const ShortcutText = styled.span`
+ color: ${lightGray};
+ font-size: ${getFontSize() - 3}px;
+ margin-right: auto;
+`;
+
+function ModeSelect() {
+ const dispatch = useAppDispatch();
+ const mode = useAppSelector(selectCurrentMode);
+ const selectedModel = useAppSelector(selectDefaultModel);
+ const agentModeSupported = selectedModel && modelSupportsTools(selectedModel);
+ const jetbrains = isJetBrains();
+
+ const getModeIcon = (mode: MessageModes) => {
+ switch (mode) {
+ case "agent":
+ return
;
+ case "chat":
+ return
;
+ case "edit":
+ return
;
+ }
+ };
+
+ // Switch to chat mode if agent mode is selected but not supported
+ useEffect(() => {
+ if (mode === "agent" && !agentModeSupported) {
+ dispatch(setMode("chat"));
+ }
+ }, [mode, agentModeSupported, dispatch]);
+
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "." && (e.metaKey || e.ctrlKey)) {
+ e.preventDefault();
+ dispatch(cycleMode({ isJetBrains: jetbrains }));
+ }
+ };
+
+ document.addEventListener("keydown", handleKeyDown);
+ return () => document.removeEventListener("keydown", handleKeyDown);
+ }, [dispatch, mode, jetbrains]);
+
+ return (
+
{
+ dispatch(setMode(newMode));
+ }}
+ >
+
+
+
+ {getModeIcon(mode)}
+ {mode.charAt(0).toUpperCase() + mode.slice(1)}
+
+
+
+
+
+
+ Agent
+ {/* */}
+ {mode === "agent" && }
+ {!agentModeSupported && (Not supported) }
+
+
+
+
+ Chat
+ {getMetaKeyLabel()}L
+ {mode === "chat" && }
+
+
+ {!jetbrains && (
+
+
+ Edit
+ {getMetaKeyLabel()}I
+ {mode === "edit" && }
+
+ )}
+
+
+ {getMetaKeyLabel()}
+ . for next mode
+
+
+
+
+ );
+}
+
+export default ModeSelect;
diff --git a/gui/src/components/modelSelection/ModelSelect.tsx b/gui/src/components/modelSelection/ModelSelect.tsx
index 3bda3b95ec..437e2304ff 100644
--- a/gui/src/components/modelSelection/ModelSelect.tsx
+++ b/gui/src/components/modelSelection/ModelSelect.tsx
@@ -1,11 +1,10 @@
import { Listbox } from "@headlessui/react";
import {
+ CheckIcon,
ChevronDownIcon,
Cog6ToothIcon,
CubeIcon,
PlusIcon,
- TrashIcon,
- CheckIcon,
} from "@heroicons/react/24/outline";
import { useContext, useEffect, useRef, useState } from "react";
import { useDispatch } from "react-redux";
@@ -20,8 +19,7 @@ import {
setDefaultModel,
} from "../../redux/slices/configSlice";
import { setDialogMessage, setShowDialog } from "../../redux/slices/uiSlice";
-import { getFontSize, isMetaEquivalentKeyPressed } from "../../util";
-import ConfirmationDialog from "../dialogs/ConfirmationDialog";
+import { fontSize, isMetaEquivalentKeyPressed } from "../../util";
import Shortcut from "../gui/Shortcut";
import { Divider } from "./platform/shared";
@@ -29,7 +27,6 @@ interface ModelOptionProps {
option: Option;
idx: number;
showMissingApiKeyMsg: boolean;
- showDelete?: boolean;
isSelected?: boolean;
}
@@ -48,7 +45,7 @@ const StyledListboxButton = styled(Listbox.Button)`
gap: 2px;
border: none;
cursor: pointer;
- font-size: ${getFontSize() - 2}px;
+ font-size: ${fontSize(-3)};
background: transparent;
color: ${lightGray};
&:focus {
@@ -67,6 +64,7 @@ const StyledListboxOptions = styled(Listbox.Options)<{ $showabove: boolean }>`
display: flex;
flex-direction: column;
+ font-size: ${fontSize(-3)};
border-radius: ${defaultBorderRadius};
border: 0.5px solid ${lightGray};
background-color: ${vscInputBackground};
@@ -81,7 +79,7 @@ const StyledListboxOptions = styled(Listbox.Options)<{ $showabove: boolean }>`
const StyledListboxOption = styled(Listbox.Option)<{ isDisabled?: boolean }>`
border-radius: ${defaultBorderRadius};
- padding: 6px 12px;
+ padding: 4px 12px;
${({ isDisabled }) =>
!isDisabled &&
@@ -115,7 +113,6 @@ const IconBase = styled.div<{ $hovered: boolean }>`
}
`;
-const StyledTrashIcon = styled(IconBase).attrs({ as: TrashIcon })``;
const StyledCog6ToothIcon = styled(IconBase).attrs({ as: Cog6ToothIcon })``;
function modelSelectTitle(model: any): string {
@@ -132,35 +129,13 @@ function modelSelectTitle(model: any): string {
function ModelOption({
option,
idx,
- showDelete,
showMissingApiKeyMsg,
isSelected,
}: ModelOptionProps) {
const ideMessenger = useContext(IdeMessengerContext);
- const dispatch = useDispatch();
const [hovered, setHovered] = useState(false);
- function onClickDelete(e: any) {
- e.stopPropagation();
- e.preventDefault();
-
- dispatch(setShowDialog(true));
- dispatch(
- setDialogMessage(
-
{
- ideMessenger.post("config/deleteModel", {
- title: option.title,
- });
- }}
- />,
- ),
- );
- }
-
function onClickGear(e: any) {
e.stopPropagation();
e.preventDefault();
@@ -187,7 +162,7 @@ function ModelOption({
-
+
{option.title}
{showMissingApiKeyMsg && (
@@ -199,12 +174,7 @@ function ModelOption({
- {showDelete && (
-
- )}
- {isSelected && (
-
- )}
+ {isSelected && }
@@ -216,7 +186,6 @@ function ModelSelect() {
const dispatch = useDispatch();
const defaultModel = useAppSelector(selectDefaultModel);
const allModels = useAppSelector((state) => state.config.config.models);
- const ideMessenger = useContext(IdeMessengerContext);
const [showAbove, setShowAbove] = useState(false);
const buttonRef = useRef(null);
const [options, setOptions] = useState([]);
@@ -342,7 +311,6 @@ function ModelSelect() {
option={option}
idx={idx}
key={idx}
- showDelete={options.length > 1}
showMissingApiKeyMsg={option.apiKey === ""}
isSelected={option.value === defaultModel?.title}
/>
@@ -360,7 +328,7 @@ function ModelSelect() {
value={"addModel" as any}
>
@@ -369,7 +337,7 @@ function ModelSelect() {
-
+
meta ' to toggle model
diff --git a/gui/src/components/modelSelection/platform/AssistantSelect.tsx b/gui/src/components/modelSelection/platform/AssistantSelect.tsx
index 3bba921f80..43caa02a93 100644
--- a/gui/src/components/modelSelection/platform/AssistantSelect.tsx
+++ b/gui/src/components/modelSelection/platform/AssistantSelect.tsx
@@ -6,19 +6,24 @@ import { useAuth } from "../../../context/Auth";
import { IdeMessengerContext } from "../../../context/IdeMessenger";
import { cycleProfile } from "../../../redux";
import { useAppDispatch } from "../../../redux/hooks";
-import { getFontSize, isMetaEquivalentKeyPressed } from "../../../util";
-import PopoverTransition from "../../mainInput/InputToolbar/PopoverTransition";
+import { fontSize, isMetaEquivalentKeyPressed } from "../../../util";
+import PopoverTransition from "../../mainInput/InputToolbar/bottom/PopoverTransition";
import AssistantIcon from "./AssistantIcon";
import { AssistantSelectOptions } from "./AssistantSelectOptions";
function AssistantSelectButton(props: { selectedProfile: ProfileDescription }) {
return (
-
-
+
-
{props.selectedProfile.title}
-
+
+ {props.selectedProfile.title}
+
+
);
}
@@ -63,10 +68,10 @@ export default function AssistantSelect() {
orgSlug: selectedOrganization?.slug,
});
}}
- className="text-lightgray hover:bg mb-1 mr-3 flex cursor-pointer items-center gap-1 whitespace-nowrap"
- style={{ fontSize: `${getFontSize() - 2}px` }}
+ className="hover:bg flex cursor-pointer select-none items-center gap-1 whitespace-nowrap text-gray-400"
+ style={{ fontSize: fontSize(-3) }}
>
-
Create your first assistant
+
Create your first assistant
);
}
@@ -77,8 +82,8 @@ export default function AssistantSelect() {
diff --git a/gui/src/editorInset/EditorInset.tsx b/gui/src/editorInset/EditorInset.tsx
deleted file mode 100644
index 3e90926b12..0000000000
--- a/gui/src/editorInset/EditorInset.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { useRef } from "react";
-import styled from "styled-components";
-import { defaultBorderRadius } from "../components";
-import TipTapEditor from "../components/mainInput/TipTapEditor";
-import useSetup from "../hooks/useSetup";
-import { selectSlashCommandComboBoxInputs } from "../redux/selectors";
-import { useAppSelector } from "../redux/hooks";
-
-const EditorInsetDiv = styled.div`
- max-width: 500px;
- position: relative;
- display: flex;
- border-radius: ${defaultBorderRadius};
- // box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.4);
-`;
-
-function EditorInset() {
- const availableSlashCommands = useAppSelector(
- selectSlashCommandComboBoxInputs,
- );
- const availableContextProviders = useAppSelector(
- (store) => store.config.config.contextProviders,
- );
-
- useSetup();
-
- const elementRef = useRef
(null);
-
- return (
-
- {
- console.log("Enter: ", e, modifiers);
- }}
- historyKey="chat"
- inputId="editor-inset"
- />
-
- );
-}
-
-export default EditorInset;
diff --git a/gui/src/editorInset/main.tsx b/gui/src/editorInset/main.tsx
deleted file mode 100644
index d15005ee79..0000000000
--- a/gui/src/editorInset/main.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from "react";
-import ReactDOM from "react-dom/client";
-import { Provider } from "react-redux";
-import "../index.css";
-import { store } from "../redux/store";
-
-import CustomPostHogProvider from "../hooks/CustomPostHogProvider";
-import EditorInset from "./EditorInset";
-
-ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
-
-
-
-
-
-
- ,
-);
diff --git a/gui/src/pages/More/MCPServersPreview.tsx b/gui/src/pages/More/MCPServersPreview.tsx
deleted file mode 100644
index 3d12a8f810..0000000000
--- a/gui/src/pages/More/MCPServersPreview.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-import {
- ArrowPathIcon,
- CircleStackIcon,
- CommandLineIcon,
- InformationCircleIcon,
- WrenchScrewdriverIcon,
-} from "@heroicons/react/24/outline";
-import { MCPServerStatus } from "core";
-import { useContext } from "react";
-import { SecondaryButton } from "../../components";
-import { ToolTip } from "../../components/gui/Tooltip";
-import { IdeMessengerContext } from "../../context/IdeMessenger";
-import { useAppDispatch, useAppSelector } from "../../redux/hooks";
-import { updateConfig } from "../../redux/slices/configSlice";
-
-interface MCPServerStatusProps {
- server: MCPServerStatus;
-}
-function MCPServerPreview({ server }: MCPServerStatusProps) {
- const ideMessenger = useContext(IdeMessengerContext);
- const config = useAppSelector((store) => store.config.config);
- const dispatch = useAppDispatch();
- const toolsTooltipId = `${server.id}-tools`;
- const promptsTooltipId = `${server.id}-prompts`;
- const resourcesTooltipId = `${server.id}-resources`;
- const errorsTooltipId = `${server.id}-errors`;
-
- async function onRefresh() {
- // optimistic config update
- dispatch(
- updateConfig({
- ...config,
- mcpServerStatuses: config.mcpServerStatuses.map((s) =>
- s.id === server.id
- ? {
- ...s,
- status: "connecting",
- }
- : s,
- ),
- }),
- );
- ideMessenger.post("mcp/reloadServer", {
- id: server.id,
- });
- }
-
- return (
-
-
{server.name}
-
-
- {server.status === "not-connected" ? (
-
- ) : server.status === "connected" ? (
-
- ) : server.status === "connecting" ? (
-
- ) : (
-
- )}
- {server.errors.length ? (
- <>
-
-
- {server.errors.map((error, idx) => (
- {error}
- ))}
- {server.errors.length === 0 ? (
- No known errors
- ) : null}
-
- >
- ) : null}
-
-
-
-
- {/* Tools */}
-
-
- {`Tools: ${server.tools.length}`}
-
-
- {server.tools.map((tool, idx) => (
- {tool.name}
- ))}
- {server.tools.length === 0 ? (
- No tools
- ) : null}
-
-
- {/* Prompts */}
-
-
- {`Prompts: ${server.prompts.length}`}
-
-
- {server.prompts.map((prompt, idx) => (
- {prompt.name}
- ))}
- {server.prompts.length === 0 ? (
- No prompts
- ) : null}
-
- {/* Resources */}
-
-
- {`Resources: ${server.resources.length}`}
-
-
- {server.resources.map((resource, idx) => (
- {resource.name}
- ))}
- {server.resources.length === 0 ? (
- No resources
- ) : null}
-
-
-
- );
-}
-function MCPServersPreview() {
- const servers = useAppSelector(
- (store) => store.config.config.mcpServerStatuses,
- );
- const ideMessenger = useContext(IdeMessengerContext);
-
- return (
-
-
-
MCP Servers
-
-
-
-
- {servers.length === 0 && (
- {
- ideMessenger.post("config/openProfile", {
- profileId: undefined,
- });
- }}
- >
- Add MCP Servers
-
- )}
-
- {servers.map((server, idx) => (
-
- ))}
-
-
- );
-}
-
-export default MCPServersPreview;
diff --git a/gui/src/pages/More/More.tsx b/gui/src/pages/More/More.tsx
deleted file mode 100644
index a528559bcd..0000000000
--- a/gui/src/pages/More/More.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import {
- ArrowTopRightOnSquareIcon,
- DocumentArrowUpIcon,
- TableCellsIcon,
-} from "@heroicons/react/24/outline";
-import { useContext } from "react";
-import { useNavigate } from "react-router-dom";
-import DocsIndexingStatuses from "../../components/indexing/DocsIndexingStatuses";
-import PageHeader from "../../components/PageHeader";
-import { IdeMessengerContext } from "../../context/IdeMessenger";
-import { useNavigationListener } from "../../hooks/useNavigationListener";
-import { useAppDispatch, useAppSelector } from "../../redux/hooks";
-import { setOnboardingCard } from "../../redux/slices/uiSlice";
-import { saveCurrentSession } from "../../redux/thunks/session";
-import { AccountButton } from "../config/AccountButton";
-import IndexingProgress from "./IndexingProgress";
-import KeyboardShortcuts from "./KeyboardShortcuts";
-import MCPServersPreview from "./MCPServersPreview";
-import MoreHelpRow from "./MoreHelpRow";
-import { RulesPreview } from "./RulesPreview";
-
-function MorePage() {
- useNavigationListener();
- const dispatch = useAppDispatch();
- const navigate = useNavigate();
- const ideMessenger = useContext(IdeMessengerContext);
- const config = useAppSelector((store) => store.config.config);
- const { disableIndexing } = config;
-
- return (
-
-
navigate("/")}
- title="Chat"
- rightContent={ }
- />
-
-
-
-
-
@codebase index
-
- Local embeddings of your codebase
-
-
- {disableIndexing ? (
-
-
- Indexing is disabled
-
-
- Open settings and toggle Disable Indexing to
- re-enable
-
-
- ) : (
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Help center
-
-
- ideMessenger.post("openUrl", "https://docs.continue.dev/")
- }
- />
-
-
- ideMessenger.post(
- "openUrl",
- "https://github.com/continuedev/continue/issues/new/choose",
- )
- }
- />
-
-
- ideMessenger.post("openUrl", "https://discord.gg/vapESyrFmJ")
- }
- />
-
- navigate("/stats")}
- />
-
- {
- navigate("/");
- // Used to clear the chat panel before showing onboarding card
- await dispatch(
- saveCurrentSession({
- openNewSession: true,
- generateTitle: true,
- }),
- );
- dispatch(setOnboardingCard({ show: true, activeTab: "Best" }));
- ideMessenger.post("showTutorial", undefined);
- }}
- />
-
-
-
-
-
Keyboard shortcuts
-
-
-
-
- );
-}
-
-export default MorePage;
diff --git a/gui/src/pages/More/index.ts b/gui/src/pages/More/index.ts
deleted file mode 100644
index 70931b8e65..0000000000
--- a/gui/src/pages/More/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./More";
diff --git a/gui/src/pages/config/AccountButton.tsx b/gui/src/pages/config/AccountButton.tsx
index 3fb8427d24..0eaf4e4719 100644
--- a/gui/src/pages/config/AccountButton.tsx
+++ b/gui/src/pages/config/AccountButton.tsx
@@ -4,9 +4,10 @@ import { Fragment } from "react";
import { SecondaryButton } from "../../components";
import { useAuth } from "../../context/Auth";
+import { ScopeSelect } from "./ScopeSelect";
export function AccountButton() {
- const { session, logout, login } = useAuth();
+ const { session, logout, login, organizations } = useAuth();
if (!session) {
return (
@@ -16,7 +17,7 @@ export function AccountButton() {
return (
-
+
@@ -37,6 +38,14 @@ export function AccountButton() {
{session.account.id}
+ {organizations.length > 0 && (
+
+
+ Organization
+
+
+
+ )}
Sign out
diff --git a/gui/src/pages/config/AccountManagement.tsx b/gui/src/pages/config/AccountManagement.tsx
deleted file mode 100644
index 029718ed8a..0000000000
--- a/gui/src/pages/config/AccountManagement.tsx
+++ /dev/null
@@ -1,150 +0,0 @@
-import { Listbox, Transition } from "@headlessui/react";
-import {
- ArrowTopRightOnSquareIcon,
- ChevronUpDownIcon,
- PlusCircleIcon,
-} from "@heroicons/react/24/outline";
-import { Fragment, useContext } from "react";
-import AssistantIcon from "../../components/modelSelection/platform/AssistantIcon";
-import { useAuth } from "../../context/Auth";
-import { IdeMessengerContext } from "../../context/IdeMessenger";
-import { selectProfileThunk } from "../../redux";
-import { useAppDispatch } from "../../redux/hooks";
-import { ScopeSelect } from "./ScopeSelect";
-
-export function AccountManagement({ hubEnabled }: { hubEnabled: boolean }) {
- const dispatch = useAppDispatch();
- const ideMessenger = useContext(IdeMessengerContext);
-
- const { session, login, profiles, selectedProfile, selectedOrganization } =
- useAuth();
-
- const changeProfileId = (id: string) => {
- dispatch(selectProfileThunk(id));
- };
-
- function handleOpenConfig() {
- if (!selectedProfile) {
- return;
- }
- ideMessenger.post("config/openProfile", {
- profileId: selectedProfile.id,
- });
- }
-
- return (
-
-
-
Configuration
- {hubEnabled ? (
- // Hub: show org selector
- session && (
-
- {`Organization`}
-
-
- )
- ) : (
- // Continue for teams: show org text
-
You are using Continue for Teams
- )}
-
- {profiles ? (
- <>
-
-
- {`${hubEnabled ? "Assistant" : "Profile"}`}
-
-
- {({ open }) => (
-
-
-
- {selectedProfile && (
-
- )}
-
- {selectedProfile?.title ?? "No Assistant Selected"}
-
-
-
-
-
-
-
-
-
- {profiles.map((option, idx) => (
-
-
-
- {option.title}
-
-
- ))}
- {hubEnabled && (
- {
- if (session) {
- ideMessenger.post("controlPlane/openUrl", {
- path: "new",
- orgSlug: selectedOrganization?.slug,
- });
- } else {
- login(false);
- }
- }}
- >
-
-
- Create new Assistant
-
-
- )}
-
-
-
- {selectedProfile && (
-
-
-
- {hubEnabled
- ? "Open Assistant configuration"
- : "View Workspace"}
-
-
- )}
-
- )}
-
-
- >
- ) : (
-
Loading...
- )}
-
-
- );
-}
diff --git a/gui/src/pages/config/HelpCenterSection.tsx b/gui/src/pages/config/HelpCenterSection.tsx
new file mode 100644
index 0000000000..6e9433d59d
--- /dev/null
+++ b/gui/src/pages/config/HelpCenterSection.tsx
@@ -0,0 +1,80 @@
+import {
+ ArrowTopRightOnSquareIcon,
+ DocumentArrowUpIcon,
+ TableCellsIcon,
+} from "@heroicons/react/24/outline";
+import { useContext } from "react";
+import { useNavigate } from "react-router-dom";
+import { IdeMessengerContext } from "../../context/IdeMessenger";
+import { useAppDispatch } from "../../redux/hooks";
+import { setOnboardingCard } from "../../redux/slices/uiSlice";
+import { saveCurrentSession } from "../../redux/thunks/session";
+import MoreHelpRow from "./MoreHelpRow";
+
+export function HelpCenterSection() {
+ const ideMessenger = useContext(IdeMessengerContext);
+ const navigate = useNavigate();
+ const dispatch = useAppDispatch();
+
+ return (
+
+
Help center
+
+
+ ideMessenger.post("openUrl", "https://docs.continue.dev/")
+ }
+ />
+
+
+ ideMessenger.post(
+ "openUrl",
+ "https://github.com/continuedev/continue/issues/new/choose",
+ )
+ }
+ />
+
+
+ ideMessenger.post("openUrl", "https://discord.gg/vapESyrFmJ")
+ }
+ />
+
+ navigate("/stats")}
+ />
+
+ {
+ navigate("/");
+ // Used to clear the chat panel before showing onboarding card
+ await dispatch(
+ saveCurrentSession({
+ openNewSession: true,
+ generateTitle: true,
+ }),
+ );
+ dispatch(setOnboardingCard({ show: true, activeTab: "Best" }));
+ ideMessenger.post("showTutorial", undefined);
+ }}
+ />
+
+
+ );
+}
diff --git a/gui/src/pages/More/IndexingProgress/IndexingProgress.tsx b/gui/src/pages/config/IndexingProgress/IndexingProgress.tsx
similarity index 100%
rename from gui/src/pages/More/IndexingProgress/IndexingProgress.tsx
rename to gui/src/pages/config/IndexingProgress/IndexingProgress.tsx
diff --git a/gui/src/pages/More/IndexingProgress/IndexingProgressBar.tsx b/gui/src/pages/config/IndexingProgress/IndexingProgressBar.tsx
similarity index 100%
rename from gui/src/pages/More/IndexingProgress/IndexingProgressBar.tsx
rename to gui/src/pages/config/IndexingProgress/IndexingProgressBar.tsx
diff --git a/gui/src/pages/More/IndexingProgress/IndexingProgressErrorText.tsx b/gui/src/pages/config/IndexingProgress/IndexingProgressErrorText.tsx
similarity index 100%
rename from gui/src/pages/More/IndexingProgress/IndexingProgressErrorText.tsx
rename to gui/src/pages/config/IndexingProgress/IndexingProgressErrorText.tsx
diff --git a/gui/src/pages/More/IndexingProgress/IndexingProgressIndicator.tsx b/gui/src/pages/config/IndexingProgress/IndexingProgressIndicator.tsx
similarity index 100%
rename from gui/src/pages/More/IndexingProgress/IndexingProgressIndicator.tsx
rename to gui/src/pages/config/IndexingProgress/IndexingProgressIndicator.tsx
diff --git a/gui/src/pages/More/IndexingProgress/IndexingProgressSubtext.tsx b/gui/src/pages/config/IndexingProgress/IndexingProgressSubtext.tsx
similarity index 100%
rename from gui/src/pages/More/IndexingProgress/IndexingProgressSubtext.tsx
rename to gui/src/pages/config/IndexingProgress/IndexingProgressSubtext.tsx
diff --git a/gui/src/pages/More/IndexingProgress/IndexingProgressTitleText.tsx b/gui/src/pages/config/IndexingProgress/IndexingProgressTitleText.tsx
similarity index 100%
rename from gui/src/pages/More/IndexingProgress/IndexingProgressTitleText.tsx
rename to gui/src/pages/config/IndexingProgress/IndexingProgressTitleText.tsx
diff --git a/gui/src/pages/More/IndexingProgress/index.ts b/gui/src/pages/config/IndexingProgress/index.ts
similarity index 100%
rename from gui/src/pages/More/IndexingProgress/index.ts
rename to gui/src/pages/config/IndexingProgress/index.ts
diff --git a/gui/src/pages/config/IndexingSettingsSection.tsx b/gui/src/pages/config/IndexingSettingsSection.tsx
new file mode 100644
index 0000000000..fcdf300d10
--- /dev/null
+++ b/gui/src/pages/config/IndexingSettingsSection.tsx
@@ -0,0 +1,26 @@
+import { useAppSelector } from "../../redux/hooks";
+import IndexingProgress from "./IndexingProgress";
+
+export function IndexingSettingsSection() {
+ const config = useAppSelector((state) => state.config.config);
+ return (
+
+
+
@codebase index
+
+ Local embeddings of your codebase
+
+
+ {config.disableIndexing ? (
+
+
Indexing is disabled
+
+ Open settings and toggle Disable Indexing to re-enable
+
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/gui/src/pages/More/KeyboardShortcuts.tsx b/gui/src/pages/config/KeyboardShortcuts.tsx
similarity index 88%
rename from gui/src/pages/More/KeyboardShortcuts.tsx
rename to gui/src/pages/config/KeyboardShortcuts.tsx
index 7c8e3a1955..072223453d 100644
--- a/gui/src/pages/More/KeyboardShortcuts.tsx
+++ b/gui/src/pages/config/KeyboardShortcuts.tsx
@@ -4,8 +4,8 @@ import {
lightGray,
vscForeground,
} from "../../components";
-import { getPlatform, isJetBrains } from "../../util";
import { ToolTip } from "../../components/gui/Tooltip";
+import { getPlatform, isJetBrains } from "../../util";
const GridDiv = styled.div`
display: grid;
@@ -14,6 +14,7 @@ const GridDiv = styled.div`
padding: 1rem 0;
justify-items: center;
align-items: center;
+ overflow-x: hidden;
`;
const StyledKeyDiv = styled.div`
@@ -64,7 +65,7 @@ function KeyboardShortcut(props: KeyboardShortcutProps) {
const shortcut = getPlatform() === "mac" ? props.mac : props.windows;
return (
-
{props.description}
+
{props.description}
{shortcut.split(" ").map((key, i) => {
return
;
@@ -201,20 +202,23 @@ const jetbrainsShortcuts: KeyboardShortcutProps[] = [
function KeyboardShortcuts() {
return (
-
- {(isJetBrains() ? jetbrainsShortcuts : vscodeShortcuts).map(
- (shortcut, i) => {
- return (
-
- );
- },
- )}
-
+
+
Keyboard shortcuts
+
+ {(isJetBrains() ? jetbrainsShortcuts : vscodeShortcuts).map(
+ (shortcut, i) => {
+ return (
+
+ );
+ },
+ )}
+
+
);
}
diff --git a/gui/src/pages/config/MCPServersPreview.tsx b/gui/src/pages/config/MCPServersPreview.tsx
new file mode 100644
index 0000000000..9404107d93
--- /dev/null
+++ b/gui/src/pages/config/MCPServersPreview.tsx
@@ -0,0 +1,170 @@
+import {
+ ArrowPathIcon,
+ CircleStackIcon,
+ CommandLineIcon,
+ InformationCircleIcon,
+ WrenchScrewdriverIcon,
+} from "@heroicons/react/24/outline";
+import { MCPServerStatus } from "core";
+import { useContext } from "react";
+import { ToolTip } from "../../components/gui/Tooltip";
+import { AddBlockButton } from "../../components/mainInput/Lump/sections/AddBlockButton";
+import { IdeMessengerContext } from "../../context/IdeMessenger";
+import { useAppDispatch, useAppSelector } from "../../redux/hooks";
+import { updateConfig } from "../../redux/slices/configSlice";
+import { fontSize } from "../../util";
+
+interface MCPServerStatusProps {
+ server: MCPServerStatus;
+}
+function MCPServerPreview({ server }: MCPServerStatusProps) {
+ const ideMessenger = useContext(IdeMessengerContext);
+ const config = useAppSelector((store) => store.config.config);
+ const dispatch = useAppDispatch();
+ const toolsTooltipId = `${server.id}-tools`;
+ const promptsTooltipId = `${server.id}-prompts`;
+ const resourcesTooltipId = `${server.id}-resources`;
+ const errorsTooltipId = `${server.id}-errors`;
+
+ async function onRefresh() {
+ // optimistic config update
+ dispatch(
+ updateConfig({
+ ...config,
+ mcpServerStatuses: config.mcpServerStatuses.map((s) =>
+ s.id === server.id
+ ? {
+ ...s,
+ status: "connecting",
+ }
+ : s,
+ ),
+ }),
+ );
+ ideMessenger.post("mcp/reloadServer", {
+ id: server.id,
+ });
+ }
+
+ return (
+
+
+ {/* Name and Status */}
+
{server.name}
+
+ {/* Error indicator if any */}
+ {server.errors.length ? (
+ <>
+
+
+ {server.errors.map((error, idx) => (
+ {error}
+ ))}
+
+ >
+ ) : null}
+
+ {/* Tools, Prompts, Resources with counts */}
+
+
+
+ {server.tools.length}
+
+ {server.tools.map((tool, idx) => (
+ {tool.name}
+ ))}
+ {server.tools.length === 0 && (
+ No tools
+ )}
+
+
+
+
+ {server.prompts.length}
+
+ {server.prompts.map((prompt, idx) => (
+ {prompt.name}
+ ))}
+ {server.prompts.length === 0 && (
+ No prompts
+ )}
+
+
+
+
+ {server.resources.length}
+
+ {server.resources.map((resource, idx) => (
+ {resource.name}
+ ))}
+ {server.resources.length === 0 && (
+ No resources
+ )}
+
+
+
+
+
+ {/* Refresh button */}
+
+
+ );
+}
+
+function MCPServersPreview() {
+ const servers = useAppSelector(
+ (store) => store.config.config.mcpServerStatuses,
+ );
+ const ideMessenger = useContext(IdeMessengerContext);
+
+ return (
+
+
+ {servers.map((server, idx) => (
+
+ ))}
+
+
+
+ );
+}
+
+export default MCPServersPreview;
diff --git a/gui/src/pages/config/ModelRoleSelector.tsx b/gui/src/pages/config/ModelRoleSelector.tsx
index d9d6196a73..bd777c44b7 100644
--- a/gui/src/pages/config/ModelRoleSelector.tsx
+++ b/gui/src/pages/config/ModelRoleSelector.tsx
@@ -1,15 +1,12 @@
import { Listbox, Transition } from "@headlessui/react";
-import {
- ChevronUpDownIcon,
- InformationCircleIcon,
-} from "@heroicons/react/24/outline";
+import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/24/outline";
import { type ModelDescription } from "core";
import { Fragment } from "react";
+import { defaultBorderRadius } from "../../components";
import { ToolTip } from "../../components/gui/Tooltip";
+import InfoHover from "../../components/InfoHover";
+import { fontSize } from "../../util";
-/**
- * A component for selecting a model for a specific role
- */
interface ModelRoleSelectorProps {
models: ModelDescription[];
selectedModel: ModelDescription | null;
@@ -32,66 +29,81 @@ const ModelRoleSelector = ({
return (
<>
- {displayName}
-
+
+ {displayName}
+
+
{description}
-
- {models.length === 0 ? (
-
- {["Chat", "Apply", "Edit"].includes(displayName)
- ? "None (defaulting to Chat model)"
- : "None"}
-
- ) : (
-
- {({ open }) => (
-
-
-
+
+ {({ open }) => (
+
+
0 ? "hover:bg-vsc-input-background cursor-pointer" : "cursor-not-allowed opacity-50"} text-vsc-foreground relative m-0 flex w-full items-center justify-between rounded-md border border-solid px-1.5 py-0.5 text-left text-sm`}
+ >
+ {models.length === 0 ? (
+ {`No ${displayName} models${["Chat", "Apply", "Edit"].includes(displayName) ? ". Using chat model" : ""}`}
+ ) : (
+
{selectedModel?.title ?? `Select ${displayName} model`}
+ )}
+ {models.length ? (
-
+
-
+ ) : null}
+
-
+
-
- {models.map((option, idx) => (
- (
+
+
-
- {option.title}
-
-
- ))}
-
-
-
- )}
-
- )}
+ {option.title}
+
+ {option.title === selectedModel?.title && (
+
+ )}
+
+ ))}
+
+
+
+ )}
+
>
);
};
diff --git a/gui/src/pages/config/More.tsx b/gui/src/pages/config/More.tsx
new file mode 100644
index 0000000000..081c6dd372
--- /dev/null
+++ b/gui/src/pages/config/More.tsx
@@ -0,0 +1,29 @@
+import { useContext } from "react";
+import { useNavigate } from "react-router-dom";
+import PageHeader from "../../components/PageHeader";
+import { IdeMessengerContext } from "../../context/IdeMessenger";
+import { useNavigationListener } from "../../hooks/useNavigationListener";
+import { useAppDispatch, useAppSelector } from "../../redux/hooks";
+import { AccountButton } from "./AccountButton";
+
+function MorePage() {
+ useNavigationListener();
+ const dispatch = useAppDispatch();
+ const navigate = useNavigate();
+ const ideMessenger = useContext(IdeMessengerContext);
+ const config = useAppSelector((store) => store.config.config);
+ const { disableIndexing } = config;
+
+ return (
+
+
navigate("/")}
+ title="Chat"
+ rightContent={ }
+ />
+
+ );
+}
+
+export default MorePage;
diff --git a/gui/src/pages/More/MoreHelpRow.tsx b/gui/src/pages/config/MoreHelpRow.tsx
similarity index 100%
rename from gui/src/pages/More/MoreHelpRow.tsx
rename to gui/src/pages/config/MoreHelpRow.tsx
diff --git a/gui/src/pages/config/UserSettingsForm.tsx b/gui/src/pages/config/UserSettingsForm.tsx
new file mode 100644
index 0000000000..f2555c4157
--- /dev/null
+++ b/gui/src/pages/config/UserSettingsForm.tsx
@@ -0,0 +1,298 @@
+import { CheckIcon, XMarkIcon } from "@heroicons/react/24/outline";
+import {
+ SharedConfigSchema,
+ modifyAnyConfigWithSharedConfig,
+} from "core/config/sharedConfig";
+import { useContext, useEffect, useState } from "react";
+import { Input } from "../../components";
+import NumberInput from "../../components/gui/NumberInput";
+import { Select } from "../../components/gui/Select";
+import ToggleSwitch from "../../components/gui/Switch";
+import { useAuth } from "../../context/Auth";
+import { IdeMessengerContext } from "../../context/IdeMessenger";
+import { useAppDispatch, useAppSelector } from "../../redux/hooks";
+import { updateConfig } from "../../redux/slices/configSlice";
+import { getFontSize } from "../../util";
+
+export function UserSettingsForm() {
+ /////// User settings section //////
+ const dispatch = useAppDispatch();
+ const { selectedProfile, controlServerBetaEnabled } = useAuth();
+ const ideMessenger = useContext(IdeMessengerContext);
+ const config = useAppSelector((state) => state.config.config);
+
+ function handleUpdate(sharedConfig: SharedConfigSchema) {
+ // Optimistic update
+ const updatedConfig = modifyAnyConfigWithSharedConfig(config, sharedConfig);
+ dispatch(updateConfig(updatedConfig));
+ // IMPORTANT no need for model role updates (separate logic for selected model roles)
+ // simply because this function won't be used to update model roles
+
+ // Actual update to core which propagates back with config update event
+ ideMessenger.post("config/updateSharedConfig", sharedConfig);
+ }
+
+ // Disable autocomplete
+ const disableAutocompleteInFiles = (
+ config.tabAutocompleteOptions?.disableInFiles ?? []
+ ).join(", ");
+ const [formDisableAutocomplete, setFormDisableAutocomplete] = useState(
+ disableAutocompleteInFiles,
+ );
+
+ useEffect(() => {
+ // Necessary so that reformatted/trimmed values don't cause dirty state
+ setFormDisableAutocomplete(disableAutocompleteInFiles);
+ }, [disableAutocompleteInFiles]);
+
+ // Workspace prompts
+ const promptPath = config.experimental?.promptPath || "";
+ const [formPromptPath, setFormPromptPath] = useState(promptPath);
+ const cancelChangePromptPath = () => {
+ setFormPromptPath(promptPath);
+ };
+ const handleSubmitPromptPath = () => {
+ handleUpdate({
+ promptPath: formPromptPath || "",
+ });
+ };
+
+ useEffect(() => {
+ // Necessary so that reformatted/trimmed values don't cause dirty state
+ setFormPromptPath(promptPath);
+ }, [promptPath]);
+
+ // TODO defaults are in multiple places, should be consolidated and probably not explicit here
+ const enableSessionTabs = config.ui?.showSessionTabs ?? false;
+ const codeWrap = config.ui?.codeWrap ?? false;
+ const showChatScrollbar = config.ui?.showChatScrollbar ?? false;
+ const formatMarkdownOutput = !(config.ui?.displayRawMarkdown ?? false);
+ const enableSessionTitles = !(config.disableSessionTitles ?? false);
+ const readResponseTTS = config.experimental?.readResponseTTS ?? false;
+
+ const allowAnonymousTelemetry = config.allowAnonymousTelemetry ?? true;
+ const enableIndexing = !(config.disableIndexing ?? false);
+
+ const useAutocompleteCache = config.tabAutocompleteOptions?.useCache ?? true;
+ const useChromiumForDocsCrawling =
+ config.experimental?.useChromiumForDocsCrawling ?? false;
+ const codeBlockToolbarPosition = config.ui?.codeBlockToolbarPosition ?? "top";
+ const useAutocompleteMultilineCompletions =
+ config.tabAutocompleteOptions?.multilineCompletions ?? "auto";
+ const fontSize = getFontSize();
+
+ const cancelChangeDisableAutocomplete = () => {
+ setFormDisableAutocomplete(disableAutocompleteInFiles);
+ };
+ const handleDisableAutocompleteSubmit = () => {
+ handleUpdate({
+ disableAutocompleteInFiles: formDisableAutocomplete
+ .split(",")
+ .map((val) => val.trim())
+ .filter((val) => !!val),
+ });
+ };
+
+ const [hubEnabled, setHubEnabled] = useState(false);
+ useEffect(() => {
+ ideMessenger.ide.getIdeSettings().then(({ continueTestEnvironment }) => {
+ setHubEnabled(continueTestEnvironment === "production");
+ });
+ }, [ideMessenger]);
+
+ return (
+
+ {!controlServerBetaEnabled || hubEnabled ? (
+
+
+
+
+
User settings
+
+
+
+
+ handleUpdate({
+ showSessionTabs: !enableSessionTabs,
+ })
+ }
+ text="Show Session Tabs"
+ />
+
+ handleUpdate({
+ codeWrap: !codeWrap,
+ })
+ }
+ text="Wrap Codeblocks"
+ />
+
+
+ handleUpdate({
+ showChatScrollbar: !showChatScrollbar,
+ })
+ }
+ text="Show Chat Scrollbar"
+ />
+
+ handleUpdate({
+ readResponseTTS: !readResponseTTS,
+ })
+ }
+ text="Text to Speech Output"
+ />
+
+ {/*
+ handleUpdate({
+ useChromiumForDocsCrawling: !useChromiumForDocsCrawling,
+ })
+ }
+ text="Use Chromium for Docs Crawling"
+ /> */}
+
+ handleUpdate({
+ disableSessionTitles: !enableSessionTitles,
+ })
+ }
+ text="Enable Session Titles"
+ />
+
+ handleUpdate({
+ displayRawMarkdown: !formatMarkdownOutput,
+ })
+ }
+ text="Format Markdown"
+ />
+
+
+ handleUpdate({
+ allowAnonymousTelemetry: !allowAnonymousTelemetry,
+ })
+ }
+ text="Allow Anonymous Telemetry"
+ />
+
+
+ handleUpdate({
+ disableIndexing: !enableIndexing,
+ })
+ }
+ text="Enable Indexing"
+ />
+
+ {/*
+ handleUpdate({
+ useAutocompleteCache: !useAutocompleteCache,
+ })
+ }
+ text="Use Autocomplete Cache"
+ /> */}
+
+
+
+ Multiline Autocompletions
+
+
+ handleUpdate({
+ useAutocompleteMultilineCompletions: e.target.value as
+ | "auto"
+ | "always"
+ | "never",
+ })
+ }
+ >
+ Auto
+ Always
+ Never
+
+
+
+
+ Font Size
+
+ handleUpdate({
+ fontSize: val,
+ })
+ }
+ min={7}
+ max={50}
+ />
+
+
+
+
+
+
+
+ ) : null}
+
+ );
+}
diff --git a/gui/src/pages/config/index.tsx b/gui/src/pages/config/index.tsx
index d4fcb40378..a336cb69c0 100644
--- a/gui/src/pages/config/index.tsx
+++ b/gui/src/pages/config/index.tsx
@@ -1,401 +1,92 @@
-import { ModelRole } from "@continuedev/config-yaml";
-import { CheckIcon, XMarkIcon } from "@heroicons/react/24/outline";
-import { ModelDescription } from "core";
import {
- SharedConfigSchema,
- modifyAnyConfigWithSharedConfig,
-} from "core/config/sharedConfig";
-import { useContext, useEffect, useState } from "react";
+ AcademicCapIcon,
+ CircleStackIcon,
+ Cog6ToothIcon,
+ QuestionMarkCircleIcon,
+} from "@heroicons/react/24/outline";
+import { useState } from "react";
import { useNavigate } from "react-router-dom";
-import { Input } from "../../components";
-import NumberInput from "../../components/gui/NumberInput";
-import { Select } from "../../components/gui/Select";
-import ToggleSwitch from "../../components/gui/Switch";
import PageHeader from "../../components/PageHeader";
-import { useAuth } from "../../context/Auth";
-import { IdeMessengerContext } from "../../context/IdeMessenger";
import { useNavigationListener } from "../../hooks/useNavigationListener";
-import { useAppDispatch, useAppSelector } from "../../redux/hooks";
-import {
- selectDefaultModel,
- setDefaultModel,
- updateConfig,
-} from "../../redux/slices/configSlice";
-import { getFontSize, isJetBrains } from "../../util";
+import { fontSize } from "../../util";
import { AccountButton } from "./AccountButton";
-import { AccountManagement } from "./AccountManagement";
-import ModelRoleSelector from "./ModelRoleSelector";
+import { HelpCenterSection } from "./HelpCenterSection";
+import { IndexingSettingsSection } from "./IndexingSettingsSection";
+import KeyboardShortcuts from "./KeyboardShortcuts";
+import { UserSettingsForm } from "./UserSettingsForm";
+
+type TabOption = {
+ id: string;
+ label: string;
+ component: React.ReactNode;
+ icon: React.ReactNode;
+};
function ConfigPage() {
useNavigationListener();
- const dispatch = useAppDispatch();
const navigate = useNavigate();
- const ideMessenger = useContext(IdeMessengerContext);
-
- const { selectedProfile, controlServerBetaEnabled } = useAuth();
-
- const [hubEnabled, setHubEnabled] = useState(false);
- useEffect(() => {
- ideMessenger.ide.getIdeSettings().then(({ continueTestEnvironment }) => {
- setHubEnabled(continueTestEnvironment === "production");
- });
- }, [ideMessenger]);
-
- // NOTE Hub takes priority over Continue for Teams
- // Since teams will be moving to hub, not vice versa
-
- /////// User settings section //////
- const config = useAppSelector((state) => state.config.config);
- const selectedChatModel = useAppSelector(selectDefaultModel);
-
- function handleUpdate(sharedConfig: SharedConfigSchema) {
- // Optimistic update
- const updatedConfig = modifyAnyConfigWithSharedConfig(config, sharedConfig);
- dispatch(updateConfig(updatedConfig));
- // IMPORTANT no need for model role updates (separate logic for selected model roles)
- // simply because this function won't be used to update model roles
-
- // Actual update to core which propagates back with config update event
- ideMessenger.post("config/updateSharedConfig", sharedConfig);
- }
-
- function handleRoleUpdate(role: ModelRole, model: ModelDescription | null) {
- if (!selectedProfile) {
- return;
- }
- // Optimistic update
- dispatch(
- updateConfig({
- ...config,
- selectedModelByRole: {
- ...config.selectedModelByRole,
- [role]: model,
- },
- }),
- );
- ideMessenger.post("config/updateSelectedModel", {
- profileId: selectedProfile.id,
- role,
- title: model?.title ?? null,
- });
- }
-
- // TODO use handleRoleUpdate for chat
- function handleChatModelSelection(model: ModelDescription | null) {
- if (!model) {
- return;
- }
- dispatch(setDefaultModel({ title: model.title }));
- }
-
- // TODO defaults are in multiple places, should be consolidated and probably not explicit here
- const enableSessionTabs = config.ui?.showSessionTabs ?? false;
- const codeWrap = config.ui?.codeWrap ?? false;
- const showChatScrollbar = config.ui?.showChatScrollbar ?? false;
- const formatMarkdownOutput = !(config.ui?.displayRawMarkdown ?? false);
- const enableSessionTitles = !(config.disableSessionTitles ?? false);
- const readResponseTTS = config.experimental?.readResponseTTS ?? false;
-
- const allowAnonymousTelemetry = config.allowAnonymousTelemetry ?? true;
- const enableIndexing = !(config.disableIndexing ?? false);
-
- const useAutocompleteCache = config.tabAutocompleteOptions?.useCache ?? true;
- const useChromiumForDocsCrawling =
- config.experimental?.useChromiumForDocsCrawling ?? false;
- const codeBlockToolbarPosition = config.ui?.codeBlockToolbarPosition ?? "top";
- const useAutocompleteMultilineCompletions =
- config.tabAutocompleteOptions?.multilineCompletions ?? "auto";
- const fontSize = getFontSize();
-
- // Disable autocomplete
- const disableAutocompleteInFiles = (
- config.tabAutocompleteOptions?.disableInFiles ?? []
- ).join(", ");
- const [formDisableAutocomplete, setFormDisableAutocomplete] = useState(
- disableAutocompleteInFiles,
- );
- const cancelChangeDisableAutocomplete = () => {
- setFormDisableAutocomplete(disableAutocompleteInFiles);
- };
- const handleDisableAutocompleteSubmit = () => {
- handleUpdate({
- disableAutocompleteInFiles: formDisableAutocomplete
- .split(",")
- .map((val) => val.trim())
- .filter((val) => !!val),
- });
- };
-
- useEffect(() => {
- // Necessary so that reformatted/trimmed values don't cause dirty state
- setFormDisableAutocomplete(disableAutocompleteInFiles);
- }, [disableAutocompleteInFiles]);
-
- const jetbrains = isJetBrains();
+ const [activeTab, setActiveTab] = useState("settings");
+
+ const tabs: TabOption[] = [
+ {
+ id: "settings",
+ label: "Settings",
+ component:
,
+ icon:
,
+ },
+ {
+ id: "indexing",
+ label: "Indexing",
+ component:
,
+ icon:
,
+ },
+ {
+ id: "help",
+ label: "Help",
+ component:
,
+ icon:
,
+ },
+ {
+ id: "shortcuts",
+ label: "Shortcuts",
+ component:
,
+ icon:
,
+ },
+ ];
return (
-
-
navigate("/")}
- title="Chat"
- rightContent={ }
- />
-
-
-
-
- {/* Model Roles as a separate section */}
-
-
-
Model Roles
-
-
handleChatModelSelection(model)}
- />
- handleRoleUpdate("autocomplete", model)}
- />
- {/* Jetbrains has a model selector inline */}
- {!jetbrains && (
- handleRoleUpdate("edit", model)}
- />
- )}
- handleRoleUpdate("apply", model)}
- />
- handleRoleUpdate("embed", model)}
- />
- handleRoleUpdate("rerank", model)}
- />
+
+
+
navigate("/")}
+ title="Chat"
+ rightContent={ }
+ />
+
+ {/* Tab Headers */}
+
+ {tabs.map((tab) => (
+
setActiveTab(tab.id)}
+ >
+ {tab.icon}
+ {tab.label}
-
+ ))}
+
- {!controlServerBetaEnabled || hubEnabled ? (
-
-
-
-
-
User settings
-
-
-
-
- handleUpdate({
- showSessionTabs: !enableSessionTabs,
- })
- }
- text="Show Session Tabs"
- />
-
- handleUpdate({
- codeWrap: !codeWrap,
- })
- }
- text="Wrap Codeblocks"
- />
-
-
- handleUpdate({
- showChatScrollbar: !showChatScrollbar,
- })
- }
- text="Show Chat Scrollbar"
- />
-
- handleUpdate({
- readResponseTTS: !readResponseTTS,
- })
- }
- text="Text to Speech Output"
- />
-
- {/*
- handleUpdate({
- useChromiumForDocsCrawling: !useChromiumForDocsCrawling,
- })
- }
- text="Use Chromium for Docs Crawling"
- /> */}
-
- handleUpdate({
- disableSessionTitles: !enableSessionTitles,
- })
- }
- text="Enable Session Titles"
- />
-
- handleUpdate({
- displayRawMarkdown: !formatMarkdownOutput,
- })
- }
- text="Format Markdown"
- />
-
-
- handleUpdate({
- allowAnonymousTelemetry: !allowAnonymousTelemetry,
- })
- }
- text="Allow Anonymous Telemetry"
- />
-
-
- handleUpdate({
- disableIndexing: !enableIndexing,
- })
- }
- text="Enable Indexing"
- />
-
- {/*
- handleUpdate({
- useAutocompleteCache: !useAutocompleteCache,
- })
- }
- text="Use Autocomplete Cache"
- /> */}
-
-
-
- Multiline Autocompletions
-
-
- handleUpdate({
- useAutocompleteMultilineCompletions: e.target
- .value as "auto" | "always" | "never",
- })
- }
- >
- Auto
- Always
- Never
-
-
-
-
- Font Size
-
- handleUpdate({
- fontSize: val,
- })
- }
- min={7}
- max={50}
- />
-
-
-
-
-
-
-
- ) : null}
+ {/* Tab Content */}
+
+ {tabs.find((tab) => tab.id === activeTab)?.component}
);
diff --git a/gui/src/pages/gui/Chat.tsx b/gui/src/pages/gui/Chat.tsx
index cd2f72d241..c752568130 100644
--- a/gui/src/pages/gui/Chat.tsx
+++ b/gui/src/pages/gui/Chat.tsx
@@ -13,25 +13,18 @@ import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { useSelector } from "react-redux";
import styled from "styled-components";
-import {
- Button,
- defaultBorderRadius,
- lightGray,
- vscBackground,
-} from "../../components";
+import { Button, lightGray, vscBackground } from "../../components";
import CodeToEditCard from "../../components/CodeToEditCard";
import FeedbackDialog from "../../components/dialogs/FeedbackDialog";
import FreeTrialOverDialog from "../../components/dialogs/FreeTrialOverDialog";
import { useFindWidget } from "../../components/find/FindWidget";
import TimelineItem from "../../components/gui/TimelineItem";
import ChatIndexingPeeks from "../../components/indexing/ChatIndexingPeeks";
+import { NewSessionButton } from "../../components/mainInput/belowMainInput/NewSessionButton";
+import ThinkingBlockPeek from "../../components/mainInput/belowMainInput/ThinkingBlockPeek";
import ContinueInputBox from "../../components/mainInput/ContinueInputBox";
-import { NewSessionButton } from "../../components/mainInput/NewSessionButton";
-import resolveEditorContent from "../../components/mainInput/resolveInput";
-import ThinkingBlockPeek from "../../components/mainInput/ThinkingBlockPeek";
-import AssistantSelect from "../../components/modelSelection/platform/AssistantSelect";
+import resolveEditorContent from "../../components/mainInput/tiptap/resolveInput";
import { useOnboardingCard } from "../../components/OnboardingCard";
-import PageHeader from "../../components/PageHeader";
import StepContainer from "../../components/StepContainer";
import AcceptRejectAllButtons from "../../components/StepContainer/AcceptRejectAllButtons";
import { TabBar } from "../../components/TabBar/TabBar";
@@ -43,11 +36,9 @@ import { selectCurrentToolCall } from "../../redux/selectors/selectCurrentToolCa
import { selectDefaultModel } from "../../redux/slices/configSlice";
import { submitEdit } from "../../redux/slices/editModeState";
import {
- clearLastEmptyResponse,
newSession,
selectIsInEditMode,
selectIsSingleRangeEditOrInsertion,
- setInactive,
} from "../../redux/slices/sessionSlice";
import {
setDialogEntryOn,
@@ -59,11 +50,7 @@ import { cancelStream } from "../../redux/thunks/cancelStream";
import { exitEditMode } from "../../redux/thunks/exitEditMode";
import { loadLastSession } from "../../redux/thunks/session";
import { streamResponseThunk } from "../../redux/thunks/streamResponse";
-import {
- getFontSize,
- getMetaKeyLabel,
- isMetaEquivalentKeyPressed,
-} from "../../util";
+import { isMetaEquivalentKeyPressed } from "../../util";
import {
FREE_TRIAL_LIMIT_REQUESTS,
incrementFreeTrialCount,
@@ -78,29 +65,6 @@ import { ToolCallButtons } from "./ToolCallDiv/ToolCallButtonsDiv";
import ToolOutput from "./ToolCallDiv/ToolOutput";
import { useAutoScroll } from "./useAutoScroll";
-const StopButton = styled.div`
- background-color: ${vscBackground};
- width: fit-content;
- margin-right: auto;
- margin-left: auto;
- font-size: ${getFontSize() - 2}px;
- border: 0.5px solid ${lightGray};
- border-radius: ${defaultBorderRadius};
- padding: 4px 8px;
- color: ${lightGray};
- cursor: pointer;
- box-shadow:
- 0 4px 6px rgba(0, 0, 0, 0.1),
- 0 1px 3px rgba(0, 0, 0, 0.08);
- transition: box-shadow 0.3s ease;
-
- &:hover {
- box-shadow:
- 0 6px 8px rgba(0, 0, 0, 0.15),
- 0 3px 6px rgba(0, 0, 0, 0.1);
- }
-`;
-
const StepsDiv = styled.div`
position: relative;
background-color: transparent;
@@ -143,7 +107,6 @@ export function Chat() {
(store) => store.config.config.ui?.showSessionTabs,
);
const defaultModel = useAppSelector(selectDefaultModel);
- const ttsActive = useAppSelector((state) => state.ui.ttsActive);
const isStreaming = useAppSelector((state) => state.session.isStreaming);
const [stepsOpen, setStepsOpen] = useState<(boolean | undefined)[]>([]);
const mainTextInputRef = useRef
(null);
@@ -323,23 +286,6 @@ export function Chat() {
return (
<>
- {showPageHeader && (
- {
- await dispatch(
- loadLastSession({ saveCurrentSession: false }),
- );
- dispatch(exitEditMode());
- }
- : undefined
- }
- rightContent={useHub && }
- />
- )}
-
{widget}
{!!showSessionTabs && }
@@ -444,30 +390,7 @@ export function Chat() {
))}
-
-
- {ttsActive && (
- {
- ideMessenger.post("tts/kill", undefined);
- }}
- >
- ■ Stop TTS
-
- )}
- {isStreaming && (
- {
- dispatch(setInactive());
- dispatch(clearLastEmptyResponse());
- }}
- >
- {getMetaKeyLabel()} ⌫ Cancel
-
- )}
-
-
+
{toolCallState?.status === "generated" &&
}
{isInEditMode && history.length === 0 &&
}
diff --git a/gui/src/pages/gui/ToolCallDiv/ToolOutput.tsx b/gui/src/pages/gui/ToolCallDiv/ToolOutput.tsx
index 46057e90c5..2289be544a 100644
--- a/gui/src/pages/gui/ToolCallDiv/ToolOutput.tsx
+++ b/gui/src/pages/gui/ToolCallDiv/ToolOutput.tsx
@@ -1,5 +1,5 @@
import { ContextItemWithId } from "core";
-import ContextItemsPeek from "../../../components/mainInput/ContextItemsPeek";
+import ContextItemsPeek from "../../../components/mainInput/belowMainInput/ContextItemsPeek";
interface ToolOutputProps {
contextItems: ContextItemWithId[];
diff --git a/gui/src/pages/migration.tsx b/gui/src/pages/migration.tsx
deleted file mode 100644
index b09389c59c..0000000000
--- a/gui/src/pages/migration.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { useNavigate } from "react-router-dom";
-import ContinueButton from "../components/mainInput/ContinueButton";
-
-function MigrationPage() {
- const navigate = useNavigate();
- return (
-
-
- Migration to config.json
-
-
-
- Continue now uses a .json config file. We hope that this takes the
- guesswork out of setting up.
-
-
-
- Your configuration should have been automatically migrated, but we
- recommend double-checking that everything looks correct.
-
-
-
- For a summary of what changed and examples of config.json,
- please see the{" "}
-
- migration walkthrough
-
- , and if you have any questions please reach out to us on{" "}
-
- Discord
-
- .
-
-
-
- Note: If you are running the server manually and have not updated the
- server, this message does not apply.
-
-
-
{
- navigate("/");
- }}
- disabled={false}
- />
-
- );
-}
-
-export default MigrationPage;
diff --git a/gui/src/redux/slices/sessionSlice.ts b/gui/src/redux/slices/sessionSlice.ts
index 2fde7aedd9..ba6ff25e14 100644
--- a/gui/src/redux/slices/sessionSlice.ts
+++ b/gui/src/redux/slices/sessionSlice.ts
@@ -634,6 +634,14 @@ export const sessionSlice = createSlice({
setMode: (state, action: PayloadAction
) => {
state.mode = action.payload;
},
+ cycleMode: (state, action: PayloadAction<{ isJetBrains: boolean }>) => {
+ const modes = action.payload.isJetBrains
+ ? ["chat", "edit", "agent"]
+ : ["chat", "agent"];
+ const currentIndex = modes.indexOf(state.mode);
+ const nextIndex = (currentIndex + 1) % modes.length;
+ state.mode = modes[nextIndex] as MessageModes;
+ },
setNewestCodeblocksForInput: (
state,
{
@@ -654,6 +662,9 @@ export const sessionSlice = createSlice({
selectIsInEditMode: (state) => {
return state.mode === "edit";
},
+ selectCurrentMode: (state) => {
+ return state.mode;
+ },
selectIsSingleRangeEditOrInsertion: (state) => {
if (state.mode !== "edit") {
return false;
@@ -668,6 +679,9 @@ export const sessionSlice = createSlice({
selectHasCodeToEdit: (state) => {
return state.codeToEdit.length > 0;
},
+ selectUseTools: (state) => {
+ return state.mode === "agent";
+ },
},
extraReducers: (builder) => {
addPassthroughCases(builder, [streamResponseThunk]);
@@ -739,13 +753,16 @@ export const {
updateSessionMetadata,
deleteSessionMetadata,
setNewestCodeblocksForInput,
+ cycleMode,
} = sessionSlice.actions;
export const {
selectIsGatheringContext,
selectIsInEditMode,
+ selectCurrentMode,
selectIsSingleRangeEditOrInsertion,
selectHasCodeToEdit,
+ selectUseTools,
} = sessionSlice.selectors;
export default sessionSlice.reducer;
diff --git a/gui/src/redux/slices/uiSlice.ts b/gui/src/redux/slices/uiSlice.ts
index d464454688..7bb7fb3fde 100644
--- a/gui/src/redux/slices/uiSlice.ts
+++ b/gui/src/redux/slices/uiSlice.ts
@@ -22,10 +22,10 @@ type UIState = {
isExploreDialogOpen: boolean;
hasDismissedExploreDialog: boolean;
shouldAddFileForEditing: boolean;
- useTools: boolean;
toolSettings: { [toolName: string]: ToolSetting };
toolGroupSettings: { [toolGroupName: string]: ToolGroupSetting };
ttsActive: boolean;
+ isBlockSettingsToolbarExpanded: boolean;
};
export const DEFAULT_TOOL_SETTING: ToolSetting = "allowedWithPermission";
@@ -43,7 +43,6 @@ export const uiSlice = createSlice({
),
shouldAddFileForEditing: false,
ttsActive: false,
- useTools: false,
toolSettings: {
[BuiltInToolNames.ReadFile]: "allowedWithoutPermission",
[BuiltInToolNames.CreateNewFile]: "allowedWithPermission",
@@ -57,6 +56,7 @@ export const uiSlice = createSlice({
toolGroupSettings: {
BUILT_IN_GROUP_NAME: "include",
},
+ isBlockSettingsToolbarExpanded: true,
} as UIState,
reducers: {
setOnboardingCard: (
@@ -90,9 +90,6 @@ export const uiSlice = createSlice({
state.hasDismissedExploreDialog = action.payload;
},
// Tools
- toggleUseTools: (state) => {
- state.useTools = !state.useTools;
- },
addTool: (state, action: PayloadAction) => {
state.toolSettings[action.payload.function.name] =
"allowedWithPermission";
@@ -127,6 +124,10 @@ export const uiSlice = createSlice({
setTTSActive: (state, { payload }: PayloadAction) => {
state.ttsActive = payload;
},
+ toggleBlockSettingsToolbar: (state) => {
+ state.isBlockSettingsToolbarExpanded =
+ !state.isBlockSettingsToolbarExpanded;
+ },
},
});
@@ -137,11 +138,11 @@ export const {
setShowDialog,
setIsExploreDialogOpen,
setHasDismissedExploreDialog,
- toggleUseTools,
toggleToolSetting,
toggleToolGroupSetting,
addTool,
setTTSActive,
+ toggleBlockSettingsToolbar,
} = uiSlice.actions;
export default uiSlice.reducer;
diff --git a/gui/src/redux/store.ts b/gui/src/redux/store.ts
index d2addfacd1..6d6c11b2b3 100644
--- a/gui/src/redux/store.ts
+++ b/gui/src/redux/store.ts
@@ -59,7 +59,7 @@ const saveSubsetFilters = [
// Don't persist any of the edit state for now
createFilter("editModeState", []),
createFilter("config", ["defaultModelTitle"]),
- createFilter("ui", ["toolSettings", "toolGroupSettings", "useTools"]),
+ createFilter("ui", ["toolSettings", "toolGroupSettings"]),
createFilter("indexing", []),
createFilter("tabs", ["tabs"]),
// Add this new filter for the profiles slice
diff --git a/gui/src/redux/thunks/gatherContext.ts b/gui/src/redux/thunks/gatherContext.ts
index 1333639782..ebd50e644e 100644
--- a/gui/src/redux/thunks/gatherContext.ts
+++ b/gui/src/redux/thunks/gatherContext.ts
@@ -7,7 +7,7 @@ import {
RangeInFile,
} from "core";
import * as URI from "uri-js";
-import resolveEditorContent from "../../components/mainInput/resolveInput";
+import resolveEditorContent from "../../components/mainInput/tiptap/resolveInput";
import { selectDefaultModel } from "../slices/configSlice";
import { ThunkApiType } from "../store";
diff --git a/gui/src/redux/thunks/streamNormalInput.ts b/gui/src/redux/thunks/streamNormalInput.ts
index 76c05f4ec5..41d7a9587f 100644
--- a/gui/src/redux/thunks/streamNormalInput.ts
+++ b/gui/src/redux/thunks/streamNormalInput.ts
@@ -7,6 +7,7 @@ import { selectDefaultModel } from "../slices/configSlice";
import {
abortStream,
addPromptCompletionPair,
+ selectUseTools,
setToolGenerated,
streamUpdate,
} from "../slices/sessionSlice";
@@ -32,15 +33,12 @@ export const streamNormalInput = createAsyncThunk<
const toolSettings = state.ui.toolSettings;
const toolGroupSettings = state.ui.toolGroupSettings;
const streamAborter = state.session.streamAborter;
- const useTools = state.ui.useTools;
+ const useTools = selectUseTools(state);
if (!defaultModel) {
throw new Error("Default model not defined");
}
- const includeTools =
- useTools &&
- modelSupportsTools(defaultModel) &&
- state.session.mode === "chat";
+ const includeTools = useTools && modelSupportsTools(defaultModel);
// Send request
const gen = extra.ideMessenger.llmStreamChat(
diff --git a/gui/src/redux/thunks/streamResponse.ts b/gui/src/redux/thunks/streamResponse.ts
index 896c2dd361..84e2edbe58 100644
--- a/gui/src/redux/thunks/streamResponse.ts
+++ b/gui/src/redux/thunks/streamResponse.ts
@@ -70,7 +70,6 @@ export const streamResponseThunk = createAsyncThunk<
await dispatch(
streamThunkWrapper(async () => {
const state = getState();
- const useTools = state.ui.useTools;
const defaultModel = selectDefaultModel(state);
const slashCommands = state.config.config.slashCommands || [];
const inputIndex = index ?? state.session.history.length; // Either given index or concat to end
diff --git a/gui/src/redux/thunks/streamResponseAfterToolCall.ts b/gui/src/redux/thunks/streamResponseAfterToolCall.ts
index 5e33a5fb0a..99554a6281 100644
--- a/gui/src/redux/thunks/streamResponseAfterToolCall.ts
+++ b/gui/src/redux/thunks/streamResponseAfterToolCall.ts
@@ -26,7 +26,6 @@ export const streamResponseAfterToolCall = createAsyncThunk<
await dispatch(
streamThunkWrapper(async () => {
const state = getState();
- const useTools = state.ui.useTools;
const initialHistory = state.session.history;
const defaultModel = selectDefaultModel(state);
diff --git a/gui/src/util/index.ts b/gui/src/util/index.ts
index 2636a3b675..f2b18105b3 100644
--- a/gui/src/util/index.ts
+++ b/gui/src/util/index.ts
@@ -52,6 +52,10 @@ export function getFontSize(): number {
return getLocalStorage("fontSize") ?? (isJetBrains() ? 15 : 14);
}
+export function fontSize(n: number): string {
+ return `${getFontSize() + n}px`;
+}
+
export function isJetBrains() {
return getLocalStorage("ide") === "jetbrains";
}
diff --git a/gui/src/util/navigation.ts b/gui/src/util/navigation.ts
index eed39333d8..8a7693b32f 100644
--- a/gui/src/util/navigation.ts
+++ b/gui/src/util/navigation.ts
@@ -3,6 +3,5 @@ export const ROUTES = {
HOME: "/",
CONFIG_ERROR: "/config-error",
CONFIG: "/config",
- MORE: "/more",
// EXAMPLE_ROUTE_WITH_PARAMS: (params: ParamsType) => `/route/${params}`,
};