Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add VSCode to OpenHands runtime and UI #4745

Merged
merged 48 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
0f12dc0
initial implementation of vscode server proxied by fastapi
xingyaoww Nov 5, 2024
e91eeb4
fix minor bugs
xingyaoww Nov 5, 2024
c4ab05b
increase timeout
xingyaoww Nov 5, 2024
d1c1291
get vscode working with eventstream runtime
xingyaoww Nov 5, 2024
1229fd1
hook vscode link with frontend
xingyaoww Nov 5, 2024
64b09db
launch the vscode on right path
xingyaoww Nov 5, 2024
1d1344f
fix linter
xingyaoww Nov 5, 2024
4550801
fix unit tests
xingyaoww Nov 5, 2024
fec0270
add from default plugins instead of hardcode
xingyaoww Nov 5, 2024
a35fdfd
Merge commit 'df9e9fca5a7f174fa51e48114c83e08a9680176a' into xw/vscode
xingyaoww Nov 5, 2024
718693d
Merge commit '74b3335b7d9c7e3743c7e4bbe872c7df1b1980ba' into xw/vscode
xingyaoww Nov 5, 2024
62103b4
move vscode enabled check to base
xingyaoww Nov 7, 2024
b2505a4
support getting vscode url for remote runtime
xingyaoww Nov 7, 2024
bf71fb3
Merge commit '1d6ef0e18ea4a0cbc983bcd748c36768bdf2727f' into xw/vscode
xingyaoww Nov 7, 2024
4b6d7eb
support vscode connection token
xingyaoww Nov 7, 2024
3b439be
fix vscode connection token
xingyaoww Nov 7, 2024
646862f
fix initial env var
xingyaoww Nov 7, 2024
dbda454
add toast in FE
xingyaoww Nov 7, 2024
1d874c4
pass vscode connection token correctly to runtime
xingyaoww Nov 7, 2024
3f6569f
add delay for vscode open
xingyaoww Nov 7, 2024
142c4b5
implement vscode connection token for eventstream
xingyaoww Nov 7, 2024
8421264
get vscode connection token from runtime
xingyaoww Nov 7, 2024
2944a93
only enable vscode when headless mode is false
xingyaoww Nov 8, 2024
e1e261f
add headless mode to runtime
xingyaoww Nov 8, 2024
3eed5a6
only get tokens after runtime is initialized
xingyaoww Nov 8, 2024
7149e7c
bump version info so remote runtime can work
xingyaoww Nov 8, 2024
ebe33aa
bump ver
xingyaoww Nov 8, 2024
3243138
Revert "bump ver"
xingyaoww Nov 8, 2024
4e7e15b
Revert "bump version info so remote runtime can work"
xingyaoww Nov 8, 2024
e65eef4
update instruction
xingyaoww Nov 8, 2024
aa0308b
Merge branch 'main' into xw/vscode
xingyaoww Nov 8, 2024
881b7f3
update description
xingyaoww Nov 8, 2024
97873ed
Merge branch 'main' into xw/vscode
xingyaoww Nov 8, 2024
dc815be
try not to duplicate vscode requirements
xingyaoww Nov 8, 2024
ff8f172
deepcopy plugins in runtime to avoid references
xingyaoww Nov 8, 2024
db12187
Move VSCode button to workspace file selector
openhands-agent Nov 8, 2024
ff41e00
cleanup old oh
xingyaoww Nov 8, 2024
7e0d086
use i18n for error message
xingyaoww Nov 8, 2024
278fcd2
fix success toast
xingyaoww Nov 8, 2024
c727932
do not line break anywhere
xingyaoww Nov 8, 2024
db7c64d
fix linter
xingyaoww Nov 8, 2024
f59ca04
Merge commit '8bfee87bcf160c4f6bf6155a6c3b077008ba715e' into xw/vscode
xingyaoww Nov 8, 2024
9890c05
Merge branch 'main' into xw/vscode
rbren Nov 9, 2024
d35ecd4
refactor: replace VS Code toast with chat message
openhands-agent Nov 9, 2024
3064055
style: fix code formatting
openhands-agent Nov 9, 2024
e16f250
style: update VS Code button to use standard blue color
openhands-agent Nov 9, 2024
0fddc96
feat: disable VS Code button until runtime is ready
openhands-agent Nov 9, 2024
6e60203
Merge commit '0cfb132ab7cf5befa08564b7bb8127321b5baa2f' into xw/vscode
xingyaoww Nov 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions frontend/src/api/open-hands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
GitHubAccessTokenResponse,
ErrorResponse,
GetConfigResponse,
GetVSCodeUrlResponse,
} from "./open-hands.types";

class OpenHands {
Expand Down Expand Up @@ -174,6 +175,14 @@ class OpenHands {
true,
);
}

/**
* Get the VSCode URL
* @returns VSCode URL
*/
static async getVSCodeUrl(): Promise<GetVSCodeUrlResponse> {
return request(`/api/vscode-url`, {}, false, false, 1);
}
}

export default OpenHands;
5 changes: 5 additions & 0 deletions frontend/src/api/open-hands.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,8 @@ export interface GetConfigResponse {
APP_MODE: "saas" | "oss";
GITHUB_CLIENT_ID: string | null;
}

export interface GetVSCodeUrlResponse {
vscode_url: string | null;
error?: string;
}
57 changes: 57 additions & 0 deletions frontend/src/assets/vscode-alt.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 54 additions & 2 deletions frontend/src/components/file-explorer/FileExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useTranslation } from "react-i18next";
import { twMerge } from "tailwind-merge";
import AgentState from "#/types/AgentState";
import { setRefreshID } from "#/state/codeSlice";
import { addAssistantMessage } from "#/state/chatSlice";
import IconButton from "../IconButton";
import ExplorerTree from "./ExplorerTree";
import toast from "#/utils/toast";
Expand All @@ -20,6 +21,7 @@ import { I18nKey } from "#/i18n/declaration";
import OpenHands from "#/api/open-hands";
import { useFiles } from "#/context/files";
import { isOpenHandsErrorResponse } from "#/api/open-hands.utils";
import VSCodeIcon from "#/assets/vscode-alt.svg?react";

interface ExplorerActionsProps {
onRefresh: () => void;
Expand Down Expand Up @@ -168,6 +170,35 @@ function FileExplorer({ error, isOpen, onToggle }: FileExplorerProps) {
}
};

const handleVSCodeClick = async (e: React.MouseEvent) => {
e.preventDefault();
try {
const response = await OpenHands.getVSCodeUrl();
if (response.vscode_url) {
dispatch(
addAssistantMessage(
"You opened VS Code. Please inform the agent of any changes you made to the workspace or environment. To avoid conflicts, it's best to pause the agent before making any changes.",
),
);
window.open(response.vscode_url, "_blank");
} else {
toast.error(
`open-vscode-error-${new Date().getTime()}`,
t(I18nKey.EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE, {
error: response.error,
}),
);
}
} catch (exp_error) {
toast.error(
`open-vscode-error-${new Date().getTime()}`,
t(I18nKey.EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE, {
error: String(exp_error),
}),
);
}
};

React.useEffect(() => {
refreshWorkspace();
}, [curAgentState]);
Expand Down Expand Up @@ -210,7 +241,7 @@ function FileExplorer({ error, isOpen, onToggle }: FileExplorerProps) {
!isOpen ? "w-12" : "w-60",
)}
>
<div className="flex flex-col relative h-full px-3 py-2">
<div className="flex flex-col relative h-full px-3 py-2 overflow-hidden">
<div className="sticky top-0 bg-neutral-800">
<div
className={twMerge(
Expand All @@ -232,7 +263,7 @@ function FileExplorer({ error, isOpen, onToggle }: FileExplorerProps) {
</div>
</div>
{!error && (
<div className="overflow-auto flex-grow">
<div className="overflow-auto flex-grow min-h-0">
<div style={{ display: !isOpen ? "none" : "block" }}>
<ExplorerTree files={paths} />
</div>
Expand All @@ -243,6 +274,27 @@ function FileExplorer({ error, isOpen, onToggle }: FileExplorerProps) {
<p className="text-neutral-300 text-sm">{error}</p>
</div>
)}
{isOpen && (
<button
xingyaoww marked this conversation as resolved.
Show resolved Hide resolved
type="button"
onClick={handleVSCodeClick}
disabled={
curAgentState === AgentState.INIT ||
curAgentState === AgentState.LOADING
}
className={twMerge(
"mt-auto mb-2 w-full h-10 text-white rounded flex items-center justify-center gap-2 transition-colors",
curAgentState === AgentState.INIT ||
curAgentState === AgentState.LOADING
? "bg-neutral-600 cursor-not-allowed"
: "bg-[#4465DB] hover:bg-[#3451C7]",
)}
aria-label="Open in VS Code"
>
<VSCodeIcon width={20} height={20} />
Open in VS Code
</button>
)}
</div>
<input
data-testid="file-input"
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/i18n/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,16 @@
"tr": "Sunucudan beklenmeyen yanıt yapısı",
"no": "Uventet responsstruktur fra serveren"
},
"EXPLORER$VSCODE_SWITCHING_MESSAGE": {
"en": "Switching to VS Code in 3 seconds...\nImportant: Please inform the agent of any changes you make in VS Code. To avoid conflicts, wait for the assistant to complete its work before making your own changes.",
"zh-CN": "3 秒后切换到 VS Code\n重要提示:请告知 OpenHands 您在 VS Code 中进行的任何更改。为了避免冲突,请在 OpenHands 完成工作后再进行自己的更改。",
"zh-TW": "3 秒後切換到 VS Code\n重要提示:請告知 OpenHands 您在 VS Code 中進行的任何更改。為避免衝突,請在 OpenHands 完成工作後再進行自己的更改。"
},
"EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE": {
"en": "Error switching to VS Code: {{error}}",
"zh-CN": "切换到 VS Code 时发生错误: {{error}}",
"zh-TW": "切換到 VS Code 時發生錯誤: {{error}}"
},
"LOAD_SESSION$MODAL_TITLE": {
"en": "Return to existing session?",
"de": "Zurück zu vorhandener Sitzung?",
Expand Down
35 changes: 14 additions & 21 deletions frontend/src/utils/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export default {
style: {
background: "#ef4444",
color: "#fff",
lineBreak: "anywhere",
},
iconTheme: {
primary: "#ef4444",
Expand All @@ -19,25 +18,20 @@ export default {
});
idMap.set(id, toastId);
},
success: (id: string, msg: string) => {
const toastId = idMap.get(id);
if (toastId === undefined) return;
if (toastId) {
toast.success(msg, {
id: toastId,
duration: 4000,
style: {
background: "#333",
color: "#fff",
lineBreak: "anywhere",
},
iconTheme: {
primary: "#333",
secondary: "#fff",
},
});
}
idMap.delete(id);
success: (id: string, msg: string, duration: number = 4000) => {
if (idMap.has(id)) return; // prevent duplicate toast
const toastId = toast.success(msg, {
duration,
style: {
background: "#333",
color: "#fff",
},
iconTheme: {
primary: "#333",
secondary: "#fff",
},
});
idMap.set(id, toastId);
},
settingsChanged: (msg: string) => {
toast(msg, {
Expand All @@ -48,7 +42,6 @@ export default {
style: {
background: "#333",
color: "#fff",
lineBreak: "anywhere",
},
});
},
Expand Down
1 change: 1 addition & 0 deletions openhands/core/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ async def main():
event_stream=event_stream,
sid=sid,
plugins=agent_cls.sandbox_plugins,
headless_mode=True,
)

controller = AgentController(
Expand Down
6 changes: 5 additions & 1 deletion openhands/core/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,14 @@ def read_task_from_stdin() -> str:
def create_runtime(
config: AppConfig,
sid: str | None = None,
headless_mode: bool = True,
) -> Runtime:
"""Create a runtime for the agent to run on.

config: The app config.
sid: The session id.
headless_mode: Whether the agent is run in headless mode. `create_runtime` is typically called within evaluation scripts,
where we don't want to have the VSCode UI open, so it defaults to True.
"""
# if sid is provided on the command line, use it as the name of the event stream
# otherwise generate it on the basis of the configured jwt_secret
Expand All @@ -80,6 +83,7 @@ def create_runtime(
event_stream=event_stream,
sid=session_id,
plugins=agent_cls.sandbox_plugins,
headless_mode=headless_mode,
)

return runtime
Expand Down Expand Up @@ -122,7 +126,7 @@ async def run_controller(
sid = sid or generate_sid(config)

if runtime is None:
runtime = create_runtime(config, sid=sid)
runtime = create_runtime(config, sid=sid, headless_mode=headless_mode)
await runtime.connect()

event_stream = runtime.event_stream
Expand Down
27 changes: 21 additions & 6 deletions openhands/runtime/action_execution_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,11 @@
from openhands.events.serialization import event_from_dict, event_to_dict
from openhands.runtime.browser import browse
from openhands.runtime.browser.browser_env import BrowserEnv
from openhands.runtime.plugins import (
ALL_PLUGINS,
JupyterPlugin,
Plugin,
)
from openhands.runtime.plugins import ALL_PLUGINS, JupyterPlugin, Plugin, VSCodePlugin
from openhands.runtime.utils.bash import BashSession
from openhands.runtime.utils.files import insert_lines, read_lines
from openhands.runtime.utils.runtime_init import init_user_and_working_directory
from openhands.runtime.utils.system import check_port_available
from openhands.utils.async_utils import wait_all


Expand Down Expand Up @@ -116,7 +113,10 @@ def initial_pwd(self):
return self._initial_pwd

async def ainit(self):
await wait_all(self._init_plugin(plugin) for plugin in self.plugins_to_load)
await wait_all(
(self._init_plugin(plugin) for plugin in self.plugins_to_load),
timeout=30,
)

# This is a temporary workaround
# TODO: refactor AgentSkills to be part of JupyterPlugin
Expand Down Expand Up @@ -345,6 +345,8 @@ def close(self):
)
# example: python client.py 8000 --working-dir /workspace --plugins JupyterRequirement
args = parser.parse_args()
os.environ['VSCODE_PORT'] = str(int(args.port) + 1)
assert check_port_available(int(os.environ['VSCODE_PORT']))

plugins_to_load: list[Plugin] = []
if args.plugins:
Expand Down Expand Up @@ -527,6 +529,19 @@ async def download_file(path: str):
async def alive():
return {'status': 'ok'}

# ================================
# VSCode-specific operations
# ================================

@app.get('/vscode/connection_token')
async def get_vscode_connection_token():
assert client is not None
if 'vscode' in client.plugins:
plugin: VSCodePlugin = client.plugins['vscode'] # type: ignore
return {'token': plugin.vscode_connection_token}
else:
return {'token': None}

# ================================
# File-specific operations for UI
# ================================
Expand Down
Loading
Loading