Compare commits

...

3 Commits

Author SHA1 Message Date
Patrick Erichsen
23a4a4a506 bump package.json version 2025-09-17 19:57:34 -07:00
Patrick Erichsen
de3d85ae54 feat: add block icons back to lump (#7789)
* feat: add block icons back to lump

* prettier

* Update index.tsx
2025-09-17 19:55:55 -07:00
Dallin Romney
1699062208 fix: HOTFIX include message content when tool calls present (#7788)
* fix: include message content when tool calls present

* fix: HOTFIX text parts for anthropic openai adapter
2025-09-17 19:55:47 -07:00
11 changed files with 227 additions and 157 deletions

View File

@@ -537,18 +537,14 @@ export abstract class BaseLLM implements ILLM {
}
private _formatChatMessage(msg: ChatMessage): string {
let contentToShow = "";
if (msg.role === "tool") {
contentToShow = msg.content;
} else if (msg.role === "assistant" && msg.toolCalls?.length) {
contentToShow = msg.toolCalls
let contentToShow = renderChatMessage(msg);
if (msg.role === "assistant" && msg.toolCalls?.length) {
contentToShow += msg.toolCalls
?.map(
(toolCall) =>
`${toolCall.function?.name}(${toolCall.function?.arguments})`,
)
.join("\n");
} else if ("content" in msg) {
contentToShow = renderChatMessage(msg);
}
return `<${msg.role}>\n${contentToShow}\n\n`;

View File

@@ -68,6 +68,17 @@ class Anthropic extends BaseLLM {
return finalOptions;
}
private buildTextPart(text: string, addCaching: boolean) {
const part: any = {
type: "text",
text,
};
if (addCaching) {
part.cache_control = { type: "ephemeral" };
}
return part;
}
private convertMessage(message: ChatMessage, addCaching: boolean): any {
if (message.role === "tool") {
return {
@@ -81,14 +92,30 @@ class Anthropic extends BaseLLM {
],
};
} else if (message.role === "assistant" && message.toolCalls) {
return {
role: "assistant",
content: message.toolCalls.map((toolCall) => ({
const parts: any[] = [];
if (message.content) {
if (typeof message.content === "string") {
parts.push(this.buildTextPart(message.content, addCaching));
} else if (message.content.length > 0) {
const textContent = message.content.filter((p) => p.type === "text");
const textParts = textContent.map((part, idx) => {
const cache = idx === textContent.length - 1 && addCaching;
return this.buildTextPart(part.text, cache);
});
parts.push(...textParts);
}
}
parts.push(
...message.toolCalls.map((toolCall) => ({
type: "tool_use",
id: toolCall.id,
name: toolCall.function?.name,
input: safeParseToolCallArgs(toolCall),
})),
);
return {
role: "assistant",
content: parts,
};
} else if (message.role === "thinking" && !message.redactedThinking) {
return {
@@ -154,9 +181,7 @@ class Anthropic extends BaseLLM {
public convertMessages(msgs: ChatMessage[]): any[] {
// should be public for use within VertexAI
const filteredmessages = msgs.filter(
(m) => m.role !== "system" && !!m.content,
);
const filteredmessages = msgs.filter((m) => m.role !== "system");
const lastTwoUserMsgIndices = filteredmessages
.map((msg, index) => (msg.role === "user" ? index : -1))
.filter((index) => index !== -1)

View File

@@ -2,7 +2,7 @@
"name": "continue",
"icon": "media/icon.png",
"author": "Continue Dev, Inc",
"version": "1.3.7",
"version": "1.2.4",
"repository": {
"type": "git",
"url": "https://github.com/continuedev/continue"

View File

@@ -1,4 +1,7 @@
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
import {
Cog6ToothIcon,
ExclamationTriangleIcon,
} from "@heroicons/react/24/outline";
import { ProfileDescription } from "core/config/ProfileLifecycleManager";
import { useContext, useMemo } from "react";
import { useNavigate } from "react-router-dom";
@@ -48,7 +51,7 @@ export function AssistantOption({
disabled={hasFatalErrors}
onClick={!hasFatalErrors ? handleOptionClick : undefined}
fontSizeModifier={-2}
className={selected ? "bg-list-active text-list-active-foreground" : ""}
className={`group ${selected ? "bg-list-active text-list-active-foreground" : ""}`}
>
<div className="flex w-full items-center justify-between gap-10 py-0.5">
<div className="flex w-full items-center gap-3">
@@ -62,6 +65,20 @@ export function AssistantOption({
</span>
</div>
<div className="flex flex-row items-center gap-1.5">
<Button
variant="ghost"
size="sm"
className="text-description-muted hover:enabled:text-foreground my-0 h-4 w-4 p-0 opacity-0 transition-opacity group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
ideMessenger.post("config/openProfile", {
profileId: profile.id,
});
onClick(); // Close the listbox
}}
>
<Cog6ToothIcon className="h-3.5 w-3.5" />
</Button>
{profile.errors && profile.errors?.length > 0 && (
<ToolTip content="View errors">
<Button

View File

@@ -1,14 +1,16 @@
import { CheckIcon } from "@heroicons/react/24/outline";
import { Cog6ToothIcon } from "@heroicons/react/24/outline";
import {
BuildingOfficeIcon,
UserCircleIcon,
UserIcon,
} from "@heroicons/react/24/solid";
import { useContext } from "react";
import { useNavigate } from "react-router-dom";
import { IdeMessengerContext } from "../../context/IdeMessenger";
import { useAppDispatch, useAppSelector } from "../../redux/hooks";
import { setSelectedOrgId } from "../../redux/slices/profilesSlice";
import { ListboxOption } from "../ui";
import { CONFIG_ROUTES } from "../../util/navigation";
import { Button, ListboxOption } from "../ui";
interface OrganizationOptionProps {
organization: { id: string; name: string; iconUrl?: string | null };
@@ -45,6 +47,7 @@ export function OrganizationOption({
onClose,
}: OrganizationOptionProps) {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const ideMessenger = useContext(IdeMessengerContext);
const selectedOrgId = useAppSelector(
(state) => state.profiles.selectedOrganizationId,
@@ -64,7 +67,7 @@ export function OrganizationOption({
value={organization.id}
onClick={handleOptionClick}
fontSizeModifier={-2}
className={isSelected ? "bg-list-active text-list-active-foreground" : ""}
className={`group ${isSelected ? "bg-list-active text-list-active-foreground" : ""}`}
>
<div className="flex w-full items-center justify-between gap-10 py-0.5">
<div className="flex w-full items-center gap-3">
@@ -78,7 +81,18 @@ export function OrganizationOption({
</span>
</div>
<div className="flex flex-row items-center gap-1.5">
{isSelected && <CheckIcon className="h-3.5 w-3.5 flex-shrink-0" />}
<Button
variant="ghost"
size="sm"
className="text-description-muted hover:enabled:text-foreground my-0 h-4 w-4 p-0 opacity-0 transition-opacity group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
navigate(CONFIG_ROUTES.ORGANIZATIONS);
onClose(); // Close the listbox
}}
>
<Cog6ToothIcon className="h-3.5 w-3.5" />
</Button>
</div>
</div>
</ListboxOption>

View File

@@ -2,9 +2,7 @@ import {
ArrowPathIcon,
ArrowRightStartOnRectangleIcon,
Cog6ToothIcon,
PencilIcon,
PlusIcon,
WrenchScrewdriverIcon,
} from "@heroicons/react/24/outline";
import { isOnPremSession } from "core/control-plane/AuthTypes";
import { useContext, useEffect, useRef } from "react";
@@ -155,7 +153,7 @@ export function AssistantAndOrgListbox({
className="max-h-32 -translate-x-1.5 overflow-y-auto pb-0"
style={{ zIndex: 200 }}
>
<div className="flex items-center justify-between p-2">
<div className="flex items-center justify-between px-1.5 py-1">
<span className="text-description text-xs font-medium">
Agents
</span>
@@ -193,7 +191,7 @@ export function AssistantAndOrgListbox({
{shouldRenderOrgInfo && (
<>
<Divider className="!mb-0.5 !mt-0" />
<div className="flex items-center justify-between p-2">
<div className="flex items-center justify-between px-1.5 py-1">
<span className="text-description text-xs font-medium">
Organizations
</span>
@@ -232,34 +230,6 @@ export function AssistantAndOrgListbox({
{/* Settings Section */}
{variant !== "sidebar" && (
<div>
<Button
onClick={(e) => {
e.stopPropagation();
onRulesConfig();
}}
variant="ghost"
size="sm"
className="text-description hover:bg-input my-0 w-full justify-start py-1.5 pl-1 text-left"
>
<div className="flex w-full items-center">
<PencilIcon className="ml-1.5 mr-2 h-3.5 w-3.5 flex-shrink-0" />
<span className="text-2xs">Rules</span>
</div>
</Button>
<Button
onClick={(e) => {
e.stopPropagation();
onToolsConfig();
}}
variant="ghost"
size="sm"
className="text-description hover:bg-input my-0 w-full justify-start py-1.5 pl-1 text-left"
>
<div className="flex w-full items-center">
<WrenchScrewdriverIcon className="ml-1.5 mr-2 h-3.5 w-3.5 flex-shrink-0" />
<span className="text-2xs">Tools</span>
</div>
</Button>
<Button
onClick={(e) => {
e.stopPropagation();

View File

@@ -1,24 +1,45 @@
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
import {
CubeIcon,
ExclamationTriangleIcon,
PencilIcon,
WrenchScrewdriverIcon,
} from "@heroicons/react/24/outline";
import { useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { IdeMessengerContext } from "../../../../context/IdeMessenger";
import { useAppSelector } from "../../../../redux/hooks";
import { useAppDispatch, useAppSelector } from "../../../../redux/hooks";
import {
selectPendingToolCalls,
selectToolCallsByStatus,
} from "../../../../redux/selectors/selectToolCalls";
import { setSelectedProfile } from "../../../../redux/slices/profilesSlice";
import FreeTrialButton from "../../../FreeTrialButton";
import { ToolTip } from "../../../gui/Tooltip";
import HoverItem from "../../InputToolbar/HoverItem";
import { usesFreeTrialApiKey } from "core/config/usesFreeTrialApiKey";
import type { FreeTrialStatus } from "core/control-plane/client";
import { useAuth } from "../../../../context/Auth";
import { getLocalStorage } from "../../../../util/localStorage";
import { CONFIG_ROUTES } from "../../../../util/navigation";
import { AssistantAndOrgListbox } from "../../../AssistantAndOrgListbox";
export function BlockSettingsTopToolbar() {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { selectedProfile } = useAuth();
const configError = useAppSelector((store) => store.config.configError);
const config = useAppSelector((state) => state.config.config);
const ideMessenger = useContext(IdeMessengerContext);
const pendingToolCalls = useAppSelector(selectPendingToolCalls);
const callingToolCalls = useAppSelector((state) =>
selectToolCallsByStatus(state, "calling"),
);
const hasActiveContent =
pendingToolCalls.length > 0 || callingToolCalls.length > 0;
const [freeTrialStatus, setFreeTrialStatus] =
useState<FreeTrialStatus | null>(null);
const hasExitedFreeTrial = getLocalStorage("hasExitedFreeTrial");
@@ -53,9 +74,39 @@ export function BlockSettingsTopToolbar() {
const shouldShowError = configError && configError?.length > 0;
const handleRulesClick = () => {
if (selectedProfile) {
dispatch(setSelectedProfile(selectedProfile.id));
ideMessenger.post("didChangeSelectedProfile", {
id: selectedProfile.id,
});
}
navigate(CONFIG_ROUTES.RULES);
};
const handleToolsClick = () => {
if (selectedProfile) {
dispatch(setSelectedProfile(selectedProfile.id));
ideMessenger.post("didChangeSelectedProfile", {
id: selectedProfile.id,
});
}
navigate(CONFIG_ROUTES.TOOLS);
};
const handleModelsClick = () => {
if (selectedProfile) {
dispatch(setSelectedProfile(selectedProfile.id));
ideMessenger.post("didChangeSelectedProfile", {
id: selectedProfile.id,
});
}
navigate(CONFIG_ROUTES.MODELS);
};
return (
<div className="flex flex-1 items-center justify-between gap-1.5">
<div>
<div className="flex items-center gap-1">
{shouldShowError && (
<ToolTip delayShow={700} content="View configuration errors">
<div
@@ -78,7 +129,30 @@ export function BlockSettingsTopToolbar() {
</div>
</ToolTip>
)}
{!hasActiveContent && (
<div className="flex items-center gap-1.5">
<ToolTip content="Configure rules">
<HoverItem onClick={handleRulesClick} px={2}>
<PencilIcon className="text-description-muted h-3 w-3 hover:brightness-125" />
</HoverItem>
</ToolTip>
<ToolTip content="Configure tools">
<HoverItem onClick={handleToolsClick} px={2}>
<WrenchScrewdriverIcon className="text-description-muted h-3 w-3 hover:brightness-125" />
</HoverItem>
</ToolTip>
<ToolTip content="Configure models">
<HoverItem onClick={handleModelsClick} px={2}>
<CubeIcon className="text-description-muted h-3 w-3 hover:brightness-125" />
</HoverItem>
</ToolTip>
</div>
)}
</div>
<ToolTip
place="top"
content={isUsingFreeTrial ? "View free trial usage" : "Select Agent"}

View File

@@ -1,6 +1,5 @@
import {
ArrowPathIcon,
CheckIcon,
ChevronDownIcon,
Cog6ToothIcon,
CubeIcon,
@@ -16,7 +15,9 @@ import { setDialogMessage, setShowDialog } from "../../redux/slices/uiSlice";
import { updateSelectedModelByRole } from "../../redux/thunks/updateSelectedModelByRole";
import { getMetaKeyLabel, isMetaEquivalentKeyPressed } from "../../util";
import { CONFIG_ROUTES } from "../../util/navigation";
import { ToolTip } from "../gui/Tooltip";
import {
Button,
Listbox,
ListboxButton,
ListboxOption,
@@ -57,6 +58,8 @@ function ModelOption({
showMissingApiKeyMsg,
isSelected,
}: ModelOptionProps) {
const navigate = useNavigate();
function handleOptionClick(e: any) {
if (showMissingApiKeyMsg) {
e.preventDefault();
@@ -64,12 +67,18 @@ function ModelOption({
}
}
function handleConfigureClick(e: React.MouseEvent) {
e.stopPropagation();
navigate(CONFIG_ROUTES.MODELS);
}
return (
<ListboxOption
key={idx}
disabled={showMissingApiKeyMsg}
value={option.value}
onClick={handleOptionClick}
className={`group ${isSelected ? "bg-list-active text-list-active-foreground" : ""}`}
>
<div className="flex w-full items-center justify-between gap-5">
<div className="flex items-center gap-2 py-0.5">
@@ -88,9 +97,14 @@ function ModelOption({
)}
</span>
</div>
<CheckIcon
className={`h-3 w-3 flex-shrink-0 ${isSelected ? "" : "invisible"}`}
/>
<Button
variant="ghost"
size="sm"
className="text-description-muted hover:enabled:text-foreground my-0 h-4 w-4 p-0 opacity-0 transition-opacity group-hover:opacity-100"
onClick={handleConfigureClick}
>
<Cog6ToothIcon className="h-3.5 w-3.5" />
</Button>
</div>
</ListboxOption>
);
@@ -245,8 +259,21 @@ function ModelSelect() {
/>
</ListboxButton>
<ListboxOptions className="min-w-[160px]">
<div className="flex items-center justify-between gap-1 px-2 py-1">
<span className="text-description font-semibold">Models</span>
<div className="flex items-center justify-between px-1.5 py-1">
<span className="text-description text-xs font-medium">Models</span>
<div className="flex items-center gap-0.5">
<Button
onClick={(e) => {
e.stopPropagation();
onClickConfigureModels(e);
}}
variant="ghost"
size="sm"
className="my-0 h-5 w-5 p-0"
>
<Cog6ToothIcon className="text-description h-3.5 w-3.5" />
</Button>
</div>
</div>
<div className="no-scrollbar max-h-[300px] overflow-y-auto">
@@ -274,36 +301,26 @@ function ModelSelect() {
{!isConfigLoading && (
<>
<Divider className="!mb-0" />
{selectedProfile?.profileType === "local" && (
<ListboxOption
key={options.length}
onClick={onClickAddModel}
value={"addModel" as any}
fontSizeModifier={-2}
className="px-2 py-2"
>
<span className="text-description text-2xs flex flex-row items-center">
<PlusIcon className="mr-1.5 h-3.5 w-3.5" />
Add Chat model
</span>
</ListboxOption>
<>
<Divider className="!mb-0" />
<ListboxOption
key={options.length}
onClick={onClickAddModel}
value={"addModel" as any}
fontSizeModifier={-2}
className="px-2 py-2"
>
<span className="text-description text-2xs flex flex-row items-center">
<PlusIcon className="mr-1.5 h-3.5 w-3.5" />
Add Chat model
</span>
</ListboxOption>
</>
)}
<ListboxOption
value="configure-models"
fontSizeModifier={-2}
className="px-2 py-2"
onClick={onClickConfigureModels}
>
<span className="text-description text-2xs flex flex-row items-center">
<Cog6ToothIcon className="mr-1.5 h-3.5 w-3.5" />
Configure models
</span>
</ListboxOption>
<Divider className="!my-0" />
<div className="text-description flex items-center justify-between gap-1.5 px-2 py-2">
<div className="text-description flex items-center justify-start p-2">
<span className="block" style={{ fontSize: tinyFont }}>
<code>{getMetaKeyLabel()}'</code> to toggle model
</span>

View File

@@ -1,57 +0,0 @@
import styled from "styled-components";
import {
defaultBorderRadius,
lightGray,
vscInputBackground,
vscListActiveBackground,
vscListActiveForeground,
} from "..";
const TopDiv = styled.div`
display: flex;
align-items: center;
text-align: center;
margin: auto;
width: fit-content;
cursor: pointer;
border: 1px solid ${lightGray};
background-color: ${vscInputBackground};
border-radius: ${defaultBorderRadius};
&:hover {
background-color: ${lightGray}55;
}
`;
const SubDiv = styled.div<{ selected: boolean }>`
text-align: center;
padding: 8px 12px;
border-radius: ${defaultBorderRadius};
transition: all 0.2s ease-in-out;
${(props) =>
props.selected &&
`
background-color: ${vscListActiveBackground};
color: ${vscListActiveForeground};
`}
`;
function Toggle(props: {
optionOne: string;
optionTwo: string;
selected: boolean;
onClick: () => void;
}) {
return (
<TopDiv onClick={props.onClick}>
<SubDiv selected={props.selected}>{props.optionOne}</SubDiv>
<SubDiv selected={!props.selected}>{props.optionTwo}</SubDiv>
</TopDiv>
);
}
export default Toggle;

View File

@@ -1,5 +1,5 @@
import { isOnPremSession } from "core/control-plane/AuthTypes";
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { AssistantAndOrgListbox } from "../../components/AssistantAndOrgListbox";
import Alert from "../../components/gui/Alert";
@@ -14,17 +14,11 @@ function ConfigPage() {
useNavigationListener();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [activeTab, setActiveTab] = useState("settings");
const [activeTab, setActiveTab] = useState(() => {
return searchParams.get("tab") || "settings";
});
const { session, organizations } = useAuth();
// Set initial tab from URL parameter
useEffect(() => {
const tab = searchParams.get("tab");
if (tab) {
setActiveTab(tab);
}
}, [searchParams]);
const allTabs = getAllTabs();
const shouldRenderOrgInfo =
session && organizations.length > 1 && !isOnPremSession(session);

View File

@@ -140,9 +140,25 @@ export class AnthropicApi implements BaseLlmApi {
],
};
} else if (message.role === "assistant" && message.tool_calls) {
return {
role: "assistant",
content: message.tool_calls.map((toolCall) => {
const parts: any[] = [];
if (message.content) {
if (typeof message.content === "string") {
parts.push({
type: "text",
text: message.content,
});
} else if (message.content.length > 0) {
parts.push(
message.content.map((c) => ({
type: "text",
text: c.type === "text" ? c.text : c.refusal,
})),
);
}
}
parts.push(
...message.tool_calls.map((toolCall) => {
// Type guard for function tool calls
if (toolCall.type === "function" && "function" in toolCall) {
return {
@@ -158,6 +174,10 @@ export class AnthropicApi implements BaseLlmApi {
throw new Error(`Unsupported tool call type: ${toolCall.type}`);
}
}),
);
return {
role: "assistant",
content: parts,
};
}