mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Add conversationUrl static variable with getter and setter methods (#8531)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
parent
7b59e81048
commit
be1ddaa57d
@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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);
|
||||
@ -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 {
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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(
|
||||
() => () => {
|
||||
|
||||
@ -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: [] },
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user