Feat multi conversations wiring (#6011)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
tofarr
2025-01-06 07:43:11 -07:00
committed by GitHub
parent efd0267919
commit cde8aad47f
19 changed files with 210 additions and 170 deletions

View File

@@ -19,9 +19,9 @@ describe("ConversationCard", () => {
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
const expectedDate = `${formatTimeDelta(new Date("2021-10-01T12:00:00Z"))} ago`;
@@ -33,20 +33,20 @@ describe("ConversationCard", () => {
within(card).getByText(expectedDate);
});
it("should render the repo if available", () => {
it("should render the selectedRepository if available", () => {
const { rerender } = render(
<ConversationCard
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
expect(
screen.queryByTestId("conversation-card-repo"),
screen.queryByTestId("conversation-card-selected-repository"),
).not.toBeInTheDocument();
rerender(
@@ -54,13 +54,13 @@ describe("ConversationCard", () => {
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo="org/repo"
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository="org/selectedRepository"
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
screen.getByTestId("conversation-card-repo");
screen.getByTestId("conversation-card-selected-repository");
});
it("should call onClick when the card is clicked", async () => {
@@ -70,9 +70,9 @@ describe("ConversationCard", () => {
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
@@ -89,9 +89,9 @@ describe("ConversationCard", () => {
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
@@ -114,9 +114,9 @@ describe("ConversationCard", () => {
onClick={onClick}
onDelete={onDelete}
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
@@ -131,21 +131,21 @@ describe("ConversationCard", () => {
expect(onDelete).toHaveBeenCalled();
});
test("clicking the repo should not trigger the onClick handler", async () => {
test("clicking the selectedRepository should not trigger the onClick handler", async () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo="org/repo"
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository="org/selectedRepository"
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
const repo = screen.getByTestId("conversation-card-repo");
await user.click(repo);
const selectedRepository = screen.getByTestId("conversation-card-selected-repository");
await user.click(selectedRepository);
expect(onClick).not.toHaveBeenCalled();
});
@@ -156,9 +156,9 @@ describe("ConversationCard", () => {
<ConversationCard
onClick={onClick}
onDelete={onDelete}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
onChangeTitle={onChangeTitle}
/>,
);
@@ -180,9 +180,9 @@ describe("ConversationCard", () => {
onClick={onClick}
onDelete={onDelete}
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
@@ -202,9 +202,9 @@ describe("ConversationCard", () => {
onClick={onClick}
onDelete={onDelete}
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
@@ -221,9 +221,9 @@ describe("ConversationCard", () => {
onClick={onClick}
onDelete={onDelete}
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
@@ -239,19 +239,19 @@ describe("ConversationCard", () => {
});
describe("state indicator", () => {
it("should render the 'cold' indicator by default", () => {
it("should render the 'STOPPED' indicator by default", () => {
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
screen.getByTestId("cold-indicator");
screen.getByTestId("STOPPED-indicator");
});
it("should render the other indicators when provided", () => {
@@ -260,15 +260,15 @@ describe("ConversationCard", () => {
onClick={onClick}
onDelete={onDelete}
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
state="warm"
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
status="RUNNING"
/>,
);
expect(screen.queryByTestId("cold-indicator")).not.toBeInTheDocument();
screen.getByTestId("warm-indicator");
expect(screen.queryByTestId("STOPPED-indicator")).not.toBeInTheDocument();
screen.getByTestId("RUNNING-indicator");
});
});
});

View File

@@ -175,7 +175,7 @@ describe("ConversationPanel", () => {
// Ensure the conversation is renamed
expect(updateUserConversationSpy).toHaveBeenCalledWith("3", {
name: "Conversation 1 Renamed",
title: "Conversation 1 Renamed",
});
});

View File

@@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest";
import { renderWithProviders } from "test-utils";
import { createRoutesStub } from "react-router";
import { Sidebar } from "#/components/features/sidebar/sidebar";
import { MULTI_CONVO_UI_IS_ENABLED } from "#/utils/constants";
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
const renderSidebar = () => {
const RouterStub = createRoutesStub([
@@ -18,7 +18,7 @@ const renderSidebar = () => {
};
describe("Sidebar", () => {
it.skipIf(!MULTI_CONVO_UI_IS_ENABLED)(
it.skipIf(!MULTI_CONVERSATION_UI)(
"should have the conversation panel open by default",
() => {
renderSidebar();
@@ -26,7 +26,7 @@ describe("Sidebar", () => {
},
);
it.skipIf(!MULTI_CONVO_UI_IS_ENABLED)(
it.skipIf(!MULTI_CONVERSATION_UI)(
"should toggle the conversation panel",
async () => {
const user = userEvent.setup();

View File

@@ -5,7 +5,7 @@ import { screen, waitFor } from "@testing-library/react";
import toast from "react-hot-toast";
import App from "#/routes/_oh.app/route";
import OpenHands from "#/api/open-hands";
import { MULTI_CONVO_UI_IS_ENABLED } from "#/utils/constants";
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
describe("App", () => {
const RouteStub = createRoutesStub([
@@ -35,7 +35,7 @@ describe("App", () => {
await screen.findByTestId("app-route");
});
it.skipIf(!MULTI_CONVO_UI_IS_ENABLED)(
it.skipIf(!MULTI_CONVERSATION_UI)(
"should call endSession if the user does not have permission to view conversation",
async () => {
const errorToastSpy = vi.spyOn(toast, "error");
@@ -59,10 +59,10 @@ describe("App", () => {
getConversationSpy.mockResolvedValue({
conversation_id: "9999",
lastUpdated: "",
name: "",
repo: "",
state: "cold",
last_updated_at: "",
title: "",
selected_repository: "",
status: "STOPPED",
});
const { rerender } = renderWithProviders(
<RouteStub initialEntries={["/conversation/9999"]} />,

View File

@@ -9,6 +9,7 @@ import {
GetVSCodeUrlResponse,
AuthenticateResponse,
Conversation,
ResultSet,
} from "./open-hands.types";
import { openHands } from "./open-hands-axios";
import { ApiSettings } from "#/services/settings";
@@ -222,8 +223,10 @@ class OpenHands {
}
static async getUserConversations(): Promise<Conversation[]> {
const { data } = await openHands.get<Conversation[]>("/api/conversations");
return data;
const { data } = await openHands.get<ResultSet<Conversation>>(
"/api/conversations?limit=9",
);
return data.results;
}
static async deleteUserConversation(conversationId: string): Promise<void> {
@@ -232,9 +235,9 @@ class OpenHands {
static async updateUserConversation(
conversationId: string,
conversation: Partial<Omit<Conversation, "id">>,
conversation: Partial<Omit<Conversation, "conversation_id">>,
): Promise<void> {
await openHands.put(`/api/conversations/${conversationId}`, conversation);
await openHands.patch(`/api/conversations/${conversationId}`, conversation);
}
static async createConversation(

View File

@@ -1,4 +1,4 @@
import { ProjectState } from "#/components/features/conversation-panel/conversation-state-indicator";
import { ProjectStatus } from "#/components/features/conversation-panel/conversation-state-indicator";
export interface ErrorResponse {
error: string;
@@ -62,8 +62,13 @@ export interface AuthenticateResponse {
export interface Conversation {
conversation_id: string;
name: string;
repo: string | null;
lastUpdated: string;
state: ProjectState;
title: string;
selected_repository: string | null;
last_updated_at: string;
status: ProjectStatus;
}
export interface ResultSet<T> {
results: T[];
next_page_id: string | null;
}

View File

@@ -18,7 +18,7 @@ export function ContextMenu({
<ul
data-testid={testId}
ref={ref}
className={cn("bg-[#404040] rounded-md w-[224px]", className)}
className={cn("bg-[#404040] rounded-md w-[140px]", className)}
>
{children}
</ul>

View File

@@ -2,7 +2,7 @@ import React from "react";
import { formatTimeDelta } from "#/utils/format-time-delta";
import { ConversationRepoLink } from "./conversation-repo-link";
import {
ProjectState,
ProjectStatus,
ConversationStateIndicator,
} from "./conversation-state-indicator";
import { ContextMenu } from "../context-menu/context-menu";
@@ -13,20 +13,20 @@ interface ProjectCardProps {
onClick: () => void;
onDelete: () => void;
onChangeTitle: (title: string) => void;
name: string;
repo: string | null;
lastUpdated: string; // ISO 8601
state?: ProjectState;
title: string;
selectedRepository: string | null;
lastUpdatedAt: string; // ISO 8601
status?: ProjectStatus;
}
export function ConversationCard({
onClick,
onDelete,
onChangeTitle,
name,
repo,
lastUpdated,
state = "cold",
title,
selectedRepository,
lastUpdatedAt,
status = "STOPPED",
}: ProjectCardProps) {
const [contextMenuVisible, setContextMenuVisible] = React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(null);
@@ -38,7 +38,13 @@ export function ConversationCard({
inputRef.current!.value = trimmed;
} else {
// reset the value if it's empty
inputRef.current!.value = name;
inputRef.current!.value = title;
}
};
const handleKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
event.currentTarget.blur();
}
};
@@ -55,47 +61,45 @@ export function ConversationCard({
<div
data-testid="conversation-card"
onClick={onClick}
className="h-[100px] w-full px-[18px] py-4 border-b border-neutral-600"
className="h-[100px] w-full px-[18px] py-4 border-b border-neutral-600 cursor-pointer"
>
<div className="flex items-center justify-between">
<div className="flex items-center justify-between space-x-1">
<input
ref={inputRef}
data-testid="conversation-card-title"
onClick={handleInputClick}
onBlur={handleBlur}
onKeyUp={handleKeyUp}
type="text"
defaultValue={name}
className="text-sm leading-6 font-semibold bg-transparent"
defaultValue={title}
className="text-sm leading-6 font-semibold bg-transparent w-full"
/>
<div className="flex items-center gap-2 relative">
<ConversationStateIndicator state={state} />
<ConversationStateIndicator status={status} />
<EllipsisButton
onClick={(event) => {
event.stopPropagation();
setContextMenuVisible((prev) => !prev);
}}
/>
{contextMenuVisible && (
<ContextMenu testId="context-menu" className="absolute left-full">
<ContextMenuListItem
testId="delete-button"
onClick={handleDelete}
>
Delete
</ContextMenuListItem>
</ContextMenu>
)}
</div>
</div>
{repo && (
{contextMenuVisible && (
<ContextMenu testId="context-menu" className="left-full float-right">
<ContextMenuListItem testId="delete-button" onClick={handleDelete}>
Delete
</ContextMenuListItem>
</ContextMenu>
)}
{selectedRepository && (
<ConversationRepoLink
repo={repo}
selectedRepository={selectedRepository}
onClick={(e) => e.stopPropagation()}
/>
)}
<p className="text-xs text-neutral-400">
<time>{formatTimeDelta(new Date(lastUpdated))} ago</time>
<time>{formatTimeDelta(new Date(lastUpdatedAt))} ago</time>
</p>
</div>
);

View File

@@ -60,7 +60,7 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
if (oldTitle !== newTitle)
updateConversation({
id: conversationId,
conversation: { name: newTitle },
conversation: { title: newTitle },
});
};
@@ -72,7 +72,7 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
return (
<div
data-testid="conversation-panel"
className="w-[350px] h-full border border-neutral-700 bg-neutral-800 rounded-xl"
className="w-[350px] h-full border border-neutral-700 bg-neutral-800 rounded-xl overflow-y-auto"
>
<div className="pt-4 px-4 flex items-center justify-between">
{location.pathname.startsWith("/conversation") && (
@@ -98,12 +98,12 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
onClick={() => handleClickCard(project.conversation_id)}
onDelete={() => handleDeleteProject(project.conversation_id)}
onChangeTitle={(title) =>
handleChangeTitle(project.conversation_id, project.name, title)
handleChangeTitle(project.conversation_id, project.title, title)
}
name={project.name}
repo={project.repo}
lastUpdated={project.lastUpdated}
state={project.state}
title={project.title}
selectedRepository={project.selected_repository}
lastUpdatedAt={project.last_updated_at}
status={project.status}
/>
))}

View File

@@ -1,21 +1,21 @@
interface ConversationRepoLinkProps {
repo: string;
selectedRepository: string;
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
}
export function ConversationRepoLink({
repo,
selectedRepository,
onClick,
}: ConversationRepoLinkProps) {
return (
<a
data-testid="conversation-card-repo"
href={`https://github.com/${repo}`}
data-testid="conversation-card-selected-repository"
href={`https://github.com/${selectedRepository}`}
target="_blank noopener noreferrer"
onClick={onClick}
className="text-xs text-neutral-400 hover:text-neutral-200"
>
{repo}
{selectedRepository}
</a>
);
}

View File

@@ -1,39 +1,25 @@
import ColdIcon from "./state-indicators/cold.svg?react";
import CoolingIcon from "./state-indicators/cooling.svg?react";
import FinishedIcon from "./state-indicators/finished.svg?react";
import RunningIcon from "./state-indicators/running.svg?react";
import WaitingIcon from "./state-indicators/waiting.svg?react";
import WarmIcon from "./state-indicators/warm.svg?react";
type SVGIcon = React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
export type ProjectState =
| "cold"
| "cooling"
| "finished"
| "running"
| "waiting"
| "warm";
export type ProjectStatus = "RUNNING" | "STOPPED";
const INDICATORS: Record<ProjectState, SVGIcon> = {
cold: ColdIcon,
cooling: CoolingIcon,
finished: FinishedIcon,
running: RunningIcon,
waiting: WaitingIcon,
warm: WarmIcon,
const INDICATORS: Record<ProjectStatus, SVGIcon> = {
STOPPED: ColdIcon,
RUNNING: RunningIcon,
};
interface ConversationStateIndicatorProps {
state: ProjectState;
status: ProjectStatus;
}
export function ConversationStateIndicator({
state,
status,
}: ConversationStateIndicatorProps) {
const StateIcon = INDICATORS[state];
const StateIcon = INDICATORS[status];
return (
<div data-testid={`${state}-indicator`}>
<div data-testid={`${status}-indicator`}>
<StateIcon />
</div>
);

View File

@@ -1,6 +1,6 @@
import React from "react";
import { useLocation } from "react-router";
import FolderIcon from "#/icons/docs.svg?react";
import { FaListUl } from "react-icons/fa";
import { useAuth } from "#/context/auth-context";
import { useGitHubUser } from "#/hooks/query/use-github-user";
import { useIsAuthed } from "#/hooks/query/use-is-authed";
@@ -16,8 +16,7 @@ import { SettingsModal } from "#/components/shared/modals/settings/settings-moda
import { useSettingsUpToDate } from "#/context/settings-up-to-date-context";
import { useSettings } from "#/hooks/query/use-settings";
import { ConversationPanel } from "../conversation-panel/conversation-panel";
import { cn } from "#/utils/utils";
import { MULTI_CONVO_UI_IS_ENABLED } from "#/utils/constants";
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
export function Sidebar() {
const location = useLocation();
@@ -32,9 +31,18 @@ export function Sidebar() {
const [settingsModalIsOpen, setSettingsModalIsOpen] = React.useState(false);
const [startNewProjectModalIsOpen, setStartNewProjectModalIsOpen] =
React.useState(false);
const [conversationPanelIsOpen, setConversationPanelIsOpen] = React.useState(
MULTI_CONVO_UI_IS_ENABLED,
);
const [conversationPanelIsOpen, setConversationPanelIsOpen] =
React.useState(false);
const conversationPanelRef = React.useRef<HTMLDivElement | null>(null);
const handleClick = (event: MouseEvent) => {
const conversationPanel = conversationPanelRef.current;
if (conversationPanelIsOpen && conversationPanel) {
if (!conversationPanel.contains(event.target as Node)) {
setConversationPanelIsOpen(false);
}
}
};
React.useEffect(() => {
// If the github token is invalid, open the account settings modal again
@@ -43,6 +51,13 @@ export function Sidebar() {
}
}, [user.isError]);
React.useEffect(() => {
document.addEventListener("click", handleClick);
return () => {
document.removeEventListener("click", handleClick);
};
}, [conversationPanelIsOpen]);
const handleAccountSettingsModalClose = () => {
// If the user closes the modal without connecting to GitHub,
// we need to log them out to clear the invalid token from the
@@ -77,16 +92,17 @@ export function Sidebar() {
/>
)}
<SettingsButton onClick={() => setSettingsModalIsOpen(true)} />
{MULTI_CONVO_UI_IS_ENABLED && (
{MULTI_CONVERSATION_UI && (
<button
data-testid="toggle-conversation-panel"
type="button"
onClick={() => setConversationPanelIsOpen((prev) => !prev)}
className={cn(
conversationPanelIsOpen ? "border-b-2 border-[#FFE165]" : "",
)}
>
<FolderIcon width={28} height={28} />
<FaListUl
width={28}
height={28}
fill={conversationPanelIsOpen ? "#FFE165" : "#FFFFFF"}
/>
</button>
)}
<DocsButton />
@@ -97,6 +113,7 @@ export function Sidebar() {
{conversationPanelIsOpen && (
<div
ref={conversationPanelRef}
className="absolute h-full left-[calc(100%+12px)] top-0 z-20" // 12px padding (sidebar parent)
>
<ConversationPanel

View File

@@ -100,6 +100,10 @@ export function WsClientProvider({
setStatus(WsClientProviderStatus.DISCONNECTED);
}
React.useEffect(() => {
lastEventRef.current = null;
}, [conversationId]);
React.useEffect(() => {
if (!conversationId) {
throw new Error("No conversation ID provided");

View File

@@ -1,11 +1,11 @@
import { useQuery } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import { MULTI_CONVO_UI_IS_ENABLED } from "#/utils/constants";
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
export const useUserConversation = (cid: string | null) =>
useQuery({
queryKey: ["user", "conversation", cid],
queryFn: () => OpenHands.getConversation(cid!),
enabled: MULTI_CONVO_UI_IS_ENABLED && !!cid,
enabled: MULTI_CONVERSATION_UI && !!cid,
retry: false,
});

View File

@@ -17,26 +17,30 @@ const userPreferences = {
const conversations: Conversation[] = [
{
conversation_id: "1",
name: "My New Project",
repo: null,
lastUpdated: new Date().toISOString(),
state: "running",
title: "My New Project",
selected_repository: null,
last_updated_at: new Date().toISOString(),
status: "RUNNING",
},
{
conversation_id: "2",
name: "Repo Testing",
repo: "octocat/hello-world",
title: "Repo Testing",
selected_repository: "octocat/hello-world",
// 2 days ago
lastUpdated: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
state: "cold",
last_updated_at: new Date(
Date.now() - 2 * 24 * 60 * 60 * 1000,
).toISOString(),
status: "STOPPED",
},
{
conversation_id: "3",
name: "Another Project",
repo: "octocat/earth",
title: "Another Project",
selected_repository: "octocat/earth",
// 5 days ago
lastUpdated: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
state: "finished",
last_updated_at: new Date(
Date.now() - 5 * 24 * 60 * 60 * 1000,
).toISOString(),
status: "STOPPED",
},
];
@@ -182,8 +186,11 @@ export const handlers = [
http.get("/api/options/config", () => HttpResponse.json({ APP_MODE: "oss" })),
http.get("/api/conversations", async () =>
HttpResponse.json(Array.from(CONVERSATIONS.values())),
http.get("/api/conversations?limit=9", async () =>
HttpResponse.json({
results: Array.from(CONVERSATIONS.values()),
next_page_id: null,
}),
),
http.delete("/api/conversations/:conversationId", async ({ params }) => {
@@ -197,7 +204,7 @@ export const handlers = [
return HttpResponse.json(null, { status: 404 });
}),
http.put(
http.patch(
"/api/conversations/:conversationId",
async ({ params, request }) => {
const { conversationId } = params;
@@ -207,10 +214,10 @@ export const handlers = [
if (conversation) {
const body = await request.json();
if (typeof body === "object" && body?.name) {
if (typeof body === "object" && body?.title) {
CONVERSATIONS.set(conversationId, {
...conversation,
name: body.name,
title: body.title,
});
return HttpResponse.json(null, { status: 200 });
}
@@ -224,10 +231,10 @@ export const handlers = [
http.post("/api/conversations", () => {
const conversation: Conversation = {
conversation_id: (Math.random() * 100).toString(),
name: "New Conversation",
repo: null,
lastUpdated: new Date().toISOString(),
state: "warm",
title: "New Conversation",
selected_repository: null,
last_updated_at: new Date().toISOString(),
status: "RUNNING",
};
CONVERSATIONS.set(conversation.conversation_id, conversation);

View File

@@ -34,7 +34,7 @@ import { useUserConversation } from "#/hooks/query/get-conversation-permissions"
import { CountBadge } from "#/components/layout/count-badge";
import { TerminalStatusLabel } from "#/components/features/terminal/terminal-status-label";
import { useSettings } from "#/hooks/query/use-settings";
import { MULTI_CONVO_UI_IS_ENABLED } from "#/utils/constants";
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
function AppContent() {
const { gitHubToken } = useAuth();
@@ -73,7 +73,7 @@ function AppContent() {
);
React.useEffect(() => {
if (MULTI_CONVO_UI_IS_ENABLED && isFetched && !conversation) {
if (MULTI_CONVERSATION_UI && isFetched && !conversation) {
toast.error(
"This conversation does not exist, or you do not have permission to access it.",
);

View File

@@ -1 +0,0 @@
export const MULTI_CONVO_UI_IS_ENABLED = false;

View File

@@ -0,0 +1,15 @@
function loadFeatureFlag(
flagName: string,
defaultValue: boolean = false,
): boolean {
try {
const stringValue =
localStorage.getItem(`FEATURE_${flagName}`) || defaultValue.toString();
const value = !!JSON.parse(stringValue);
return value;
} catch (e) {
return defaultValue;
}
}
export const MULTI_CONVERSATION_UI = loadFeatureFlag("MULTI_CONVERSATION_UI");

View File

@@ -58,7 +58,7 @@ class SandboxConfig:
runtime_startup_env_vars: dict[str, str] = field(default_factory=dict)
browsergym_eval_env: str | None = None
platform: str | None = None
close_delay: int = 15
close_delay: int = 900
remote_runtime_resource_factor: int = 1
enable_gpu: bool = False