Add conversationUrl static variable with getter and setter methods (#8531)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
tofarr 2025-05-19 11:28:07 -06:00 committed by GitHub
parent 7b59e81048
commit be1ddaa57d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 143 additions and 40 deletions

View File

@ -45,6 +45,8 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-01T12:00:00Z",
created_at: "2021-10-01T12:00:00Z",
status: "STOPPED" as const,
url: null,
session_api_key: null,
},
{
conversation_id: "2",
@ -53,6 +55,8 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-02T12:00:00Z",
created_at: "2021-10-02T12:00:00Z",
status: "STOPPED" as const,
url: null,
session_api_key: null,
},
{
conversation_id: "3",
@ -61,6 +65,8 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-03T12:00:00Z",
created_at: "2021-10-03T12:00:00Z",
status: "STOPPED" as const,
url: null,
session_api_key: null,
},
];
@ -143,6 +149,8 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-01T12:00:00Z",
created_at: "2021-10-01T12:00:00Z",
status: "STOPPED" as const,
url: null,
session_api_key: null,
},
{
conversation_id: "2",
@ -151,6 +159,8 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-02T12:00:00Z",
created_at: "2021-10-02T12:00:00Z",
status: "STOPPED" as const,
url: null,
session_api_key: null,
},
{
conversation_id: "3",
@ -159,6 +169,8 @@ describe("ConversationPanel", () => {
last_updated_at: "2021-10-03T12:00:00Z",
created_at: "2021-10-03T12:00:00Z",
status: "STOPPED" as const,
url: null,
session_api_key: null,
},
];

View File

@ -56,6 +56,19 @@ function TestComponent() {
describe("WsClientProvider", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mock("#/hooks/query/use-user-conversation", () => ({
useUserConversation: () => {
return { data: {
conversation_id: "1",
title: "Conversation 1",
selected_repository: null,
last_updated_at: "2021-10-01T12:00:00Z",
created_at: "2021-10-01T12:00:00Z",
status: "STOPPED" as const,
url: null,
session_api_key: null,
}}},
}));
});
it("should emit oh_user_action event when send is called", async () => {

View File

@ -1,7 +1,9 @@
import OpenHands from "#/api/open-hands";
/**
* Returns a URL compatible for the file service
* @param conversationId ID of the conversation
* @returns URL of the conversation
*/
export const getConversationUrl = (conversationId: string) =>
`/api/conversations/${conversationId}`;
OpenHands.getConversationUrl(conversationId);

View File

@ -1,6 +1,6 @@
import { openHands } from "../open-hands-axios";
import { GetFilesResponse, GetFileResponse } from "./file-service.types";
import { getConversationUrl } from "./file-service.utils";
import { getConversationUrl } from "../conversation.utils";
export class FileService {
/**

View File

@ -1,3 +1,4 @@
import { AxiosHeaders } from "axios";
import {
Feedback,
FeedbackResponse,
@ -17,6 +18,38 @@ import { GitUser, GitRepository, Branch } from "#/types/git";
import { SuggestedTask } from "#/components/features/home/tasks/task.types";
class OpenHands {
private static currentConversation: Conversation | null = null;
/**
* Get a current conversation
* @return the current conversation
*/
static getCurrentConversation(): Conversation | null {
return this.currentConversation;
}
/**
* Set a current conversation
* @param url Custom URL to use for conversation endpoints
*/
static setCurrentConversation(
currentConversation: Conversation | null,
): void {
this.currentConversation = currentConversation;
}
/**
* Get the url for the conversation. If
*/
static getConversationUrl(conversationId: string): string {
if (this.currentConversation?.conversation_id === conversationId) {
if (this.currentConversation.url) {
return this.currentConversation.url;
}
}
return `/api/conversations/${conversationId}`;
}
/**
* Retrieve the list of models available
* @returns List of models available
@ -53,6 +86,15 @@ class OpenHands {
return data;
}
static getConversationHeaders(): AxiosHeaders {
const headers = new AxiosHeaders();
const sessionApiKey = this.currentConversation?.session_api_key;
if (sessionApiKey) {
headers.set("X-Session-API-Key", sessionApiKey);
}
return headers;
}
/**
* Send feedback to the server
* @param data Feedback data
@ -86,13 +128,26 @@ class OpenHands {
* @returns Blob of the workspace zip
*/
static async getWorkspaceZip(conversationId: string): Promise<Blob> {
const url = `/api/conversations/${conversationId}/zip-directory`;
const url = `${this.getConversationUrl(conversationId)}/zip-directory`;
const response = await openHands.get(url, {
responseType: "blob",
headers: this.getConversationHeaders(),
});
return response.data;
}
/**
* Get the web hosts
* @returns Array of web hosts
*/
static async getWebHosts(conversationId: string): Promise<string[]> {
const url = `${this.getConversationUrl(conversationId)}/web-hosts`;
const response = await openHands.get(url, {
headers: this.getConversationHeaders(),
});
return Object.keys(response.data.hosts);
}
/**
* @param code Code provided by GitHub
* @returns GitHub access token
@ -116,18 +171,20 @@ class OpenHands {
static async getVSCodeUrl(
conversationId: string,
): Promise<GetVSCodeUrlResponse> {
const { data } = await openHands.get<GetVSCodeUrlResponse>(
`/api/conversations/${conversationId}/vscode-url`,
);
const url = `${this.getConversationUrl(conversationId)}/vscode-url`;
const { data } = await openHands.get<GetVSCodeUrlResponse>(url, {
headers: this.getConversationHeaders(),
});
return data;
}
static async getRuntimeId(
conversationId: string,
): Promise<{ runtime_id: string }> {
const { data } = await openHands.get<{ runtime_id: string }>(
`/api/conversations/${conversationId}/config`,
);
const url = `${this.getConversationUrl(conversationId)}/config`;
const { data } = await openHands.get<{ runtime_id: string }>(url, {
headers: this.getConversationHeaders(),
});
return data;
}
@ -259,9 +316,10 @@ class OpenHands {
static async getTrajectory(
conversationId: string,
): Promise<GetTrajectoryResponse> {
const { data } = await openHands.get<GetTrajectoryResponse>(
`/api/conversations/${conversationId}/trajectory`,
);
const url = `${this.getConversationUrl(conversationId)}/trajectory`;
const { data } = await openHands.get<GetTrajectoryResponse>(url, {
headers: this.getConversationHeaders(),
});
return data;
}
@ -272,9 +330,10 @@ class OpenHands {
}
static async getGitChanges(conversationId: string): Promise<GitChange[]> {
const { data } = await openHands.get<GitChange[]>(
`/api/conversations/${conversationId}/git/changes`,
);
const url = `${this.getConversationUrl(conversationId)}/git/changes`;
const { data } = await openHands.get<GitChange[]>(url, {
headers: this.getConversationHeaders(),
});
return data;
}
@ -282,12 +341,11 @@ class OpenHands {
conversationId: string,
path: string,
): Promise<GitChangeDiff> {
const { data } = await openHands.get<GitChangeDiff>(
`/api/conversations/${conversationId}/git/diff`,
{
params: { path },
},
);
const url = `${this.getConversationUrl(conversationId)}/git/diff`;
const { data } = await openHands.get<GitChangeDiff>(url, {
params: { path },
headers: this.getConversationHeaders(),
});
return data;
}

View File

@ -80,6 +80,8 @@ export interface Conversation {
created_at: string;
status: ProjectStatus;
trigger?: ConversationTrigger;
url: string | null;
session_api_key: string | null;
}
export interface ResultSet<T> {

View File

@ -16,6 +16,7 @@ import { BaseModal } from "../../shared/modals/base-modal/base-modal";
import { RootState } from "#/store";
import { I18nKey } from "#/i18n/declaration";
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
import OpenHands from "#/api/open-hands";
import { useWsClient } from "#/context/ws-client-provider";
import { isSystemMessage } from "#/types/core/guards";
@ -115,11 +116,7 @@ export function ConversationCard({
// Fetch the VS Code URL from the API
if (conversationId) {
try {
const response = await fetch(
`/api/conversations/${conversationId}/vscode-url`,
);
const data = await response.json();
const data = await OpenHands.getVSCodeUrl(conversationId);
if (data.vscode_url) {
const transformedUrl = transformVSCodeUrl(data.vscode_url);
if (transformedUrl) {

View File

@ -16,6 +16,7 @@ import {
} from "#/types/core/actions";
import { Conversation } from "#/api/open-hands.types";
import { useUserProviders } from "#/hooks/use-user-providers";
import { useUserConversation } from "#/hooks/query/use-user-conversation";
import { OpenHandsObservation } from "#/types/core/observations";
import {
isErrorObservation,
@ -146,6 +147,7 @@ export function WsClientProvider({
const { providers } = useUserProviders();
const messageRateHandler = useRate({ threshold: 250 });
const { data: conversation } = useUserConversation(conversationId);
function send(event: Record<string, unknown>) {
if (!sioRef.current) {
@ -262,6 +264,9 @@ export function WsClientProvider({
if (!conversationId) {
throw new Error("No conversation ID provided");
}
if (!conversation) {
return () => undefined; // conversation not yet loaded
}
let sio = sioRef.current;
@ -270,10 +275,15 @@ export function WsClientProvider({
latest_event_id: lastEvent?.id ?? -1,
conversation_id: conversationId,
providers_set: providers,
session_api_key: conversation.session_api_key, // Have to set here because socketio doesn't support custom headers. :(
};
const baseUrl =
import.meta.env.VITE_BACKEND_BASE_URL || window?.location.host;
let baseUrl = null;
if (conversation.url && !conversation.url.startsWith("/")) {
baseUrl = new URL(conversation.url).host;
} else {
baseUrl = import.meta.env.VITE_BACKEND_BASE_URL || window?.location.host;
}
sio = io(baseUrl, {
transports: ["websocket"],
@ -294,7 +304,7 @@ export function WsClientProvider({
sio.off("connect_failed", handleError);
sio.off("disconnect", handleDisconnect);
};
}, [conversationId]);
}, [conversationId, conversation?.url]);
React.useEffect(
() => () => {

View File

@ -2,7 +2,7 @@ import { useQueries, useQuery } from "@tanstack/react-query";
import axios from "axios";
import React from "react";
import { useSelector } from "react-redux";
import { openHands } from "#/api/open-hands-axios";
import OpenHands from "#/api/open-hands";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { RootState } from "#/store";
import { useConversation } from "#/context/conversation-context";
@ -16,10 +16,8 @@ export const useActiveHost = () => {
const { data } = useQuery({
queryKey: [conversationId, "hosts"],
queryFn: async () => {
const response = await openHands.get<{ hosts: string[] }>(
`/api/conversations/${conversationId}/web-hosts`,
);
return { hosts: Object.keys(response.data.hosts) };
const hosts = await OpenHands.getWebHosts(conversationId);
return { hosts };
},
enabled: !RUNTIME_INACTIVE_STATES.includes(curAgentState),
initialData: { hosts: [] },

View File

@ -4,7 +4,11 @@ import OpenHands from "#/api/open-hands";
export const useUserConversation = (cid: string | null) =>
useQuery({
queryKey: ["user", "conversation", cid],
queryFn: () => OpenHands.getConversation(cid!),
queryFn: async () => {
const conversation = await OpenHands.getConversation(cid!);
OpenHands.setCurrentConversation(conversation);
return conversation;
},
enabled: !!cid,
retry: false,
staleTime: 1000 * 60 * 5, // 5 minutes

View File

@ -54,6 +54,8 @@ const conversations: Conversation[] = [
last_updated_at: new Date().toISOString(),
created_at: new Date().toISOString(),
status: "RUNNING",
url: null,
session_api_key: null,
},
{
conversation_id: "2",
@ -65,6 +67,8 @@ const conversations: Conversation[] = [
).toISOString(),
created_at: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
status: "STOPPED",
url: null,
session_api_key: null,
},
{
conversation_id: "3",
@ -76,6 +80,8 @@ const conversations: Conversation[] = [
).toISOString(),
created_at: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
status: "STOPPED",
url: null,
session_api_key: null,
},
];
@ -267,6 +273,8 @@ export const handlers = [
last_updated_at: new Date().toISOString(),
created_at: new Date().toISOString(),
status: "RUNNING",
url: null,
session_api_key: null,
};
CONVERSATIONS.set(conversation.conversation_id, conversation);

View File

@ -37,6 +37,7 @@ import { RootState } from "#/store";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { useDocumentTitleFromState } from "#/hooks/use-document-title-from-state";
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
import OpenHands from "#/api/open-hands";
import { TabContent } from "#/components/layout/tab-content";
function AppContent() {
@ -138,10 +139,8 @@ function AppContent() {
e.stopPropagation();
if (conversationId) {
try {
const response = await fetch(
`/api/conversations/${conversationId}/vscode-url`,
);
const data = await response.json();
const data =
await OpenHands.getVSCodeUrl(conversationId);
if (data.vscode_url) {
const transformedUrl = transformVSCodeUrl(
data.vscode_url,

View File

@ -485,7 +485,7 @@ class StandaloneConversationManager(ConversationManager):
)
def _get_conversation_url(self, conversation_id: str):
return f"/conversations/{conversation_id}"
return f"/api/conversations/{conversation_id}"
def _last_updated_at_key(conversation: ConversationMetadata) -> float: