Skip to content

Commit 59d7ae4

Browse files
author
yanan.zhangyn
committed
fix(studio): restore fast updates and reuse harness sessions
1 parent bad8248 commit 59d7ae4

77 files changed

Lines changed: 385 additions & 125 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/src/ui/SandboxAgentWorkspace.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@
115115
background: hsl(var(--panel));
116116
}
117117

118+
.sandbox-agent-workspace-surface [hidden] {
119+
display: none;
120+
}
121+
118122
.sandbox-agent-workspace-state {
119123
display: grid;
120124
height: 100%;

frontend/src/ui/SandboxAgentWorkspace.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,28 +108,29 @@ export function SandboxAgentWorkspace({
108108
</header>
109109

110110
<div className="sandbox-agent-workspace-surface">
111-
{surface === "main" ? (
111+
<iframe
112+
src={workspace.webuiUrl}
113+
title={t("agentWorkspace.mainTitle", { agent: label })}
114+
allow="clipboard-read; clipboard-write"
115+
hidden={surface !== "main"}
116+
/>
117+
{terminalUrl ? (
112118
<iframe
113-
src={workspace.webuiUrl}
114-
title={t("agentWorkspace.mainTitle", { agent: label })}
115-
allow="clipboard-read; clipboard-write"
119+
src={terminalUrl}
120+
title={t("agentWorkspace.terminalTitle", { agent: label })}
121+
hidden={surface !== "terminal"}
116122
/>
117-
) : terminalLoading ? (
123+
) : surface === "terminal" && terminalLoading ? (
118124
<div className="sandbox-agent-workspace-state" role="status">
119125
{t("agentWorkspace.openingTerminal")}
120126
</div>
121-
) : terminalError ? (
127+
) : surface === "terminal" && terminalError ? (
122128
<div className="sandbox-agent-workspace-state is-error" role="alert">
123129
<p>{terminalError}</p>
124130
<button type="button" onClick={() => void openTerminal()}>
125131
{t("common.tryAgain")}
126132
</button>
127133
</div>
128-
) : terminalUrl ? (
129-
<iframe
130-
src={terminalUrl}
131-
title={t("agentWorkspace.terminalTitle", { agent: label })}
132-
/>
133134
) : null}
134135
</div>
135136
</section>

frontend/tests/sandboxSessionPresentation.test.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ test("sandbox agents expose detail deletion and reusable workspaces", () => {
192192
assert.match(workspaceSource, /t\("agentWorkspace\.main"\)/);
193193
assert.match(workspaceSource, /t\("agentWorkspace\.terminal"\)/);
194194
assert.match(workspaceSource, /sandboxClient\.launchAgentTerminal/);
195+
assert.match(workspaceSource, /hidden=\{surface !== "main"\}/);
196+
assert.match(workspaceSource, /hidden=\{surface !== "terminal"\}/);
197+
assert.doesNotMatch(workspaceSource, /surface === "main"\s*\?\s*\(\s*<iframe/);
198+
assert.match(workspaceStyles, /\.sandbox-agent-workspace-surface \[hidden\][\s\S]*?display: none/);
195199
assert.match(workspaceSource, /size="lg"/);
196200
assert.match(workspaceSource, /gutterSize="lg"/);
197201
assert.match(workspaceSource, /block/);

tests/cli/test_frontend_agent_proxy.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,3 +444,46 @@ def _target(kind: str, session_id: str, token: str) -> SandboxProxyTarget:
444444

445445
assert response.status_code == 200
446446
assert forwarded_headers[0]["x-hermes-session-token"] == "hermes-session-token"
447+
448+
449+
def test_agent_surface_proxy_reuses_one_http_client(
450+
monkeypatch: pytest.MonkeyPatch,
451+
) -> None:
452+
created = 0
453+
requests = 0
454+
closed = 0
455+
456+
class _Client:
457+
def __init__(self, **_: object) -> None:
458+
nonlocal created
459+
created += 1
460+
461+
async def request(self, _method: str, _url: str, **_: object) -> httpx.Response:
462+
nonlocal requests
463+
requests += 1
464+
return httpx.Response(
465+
200,
466+
content=b"ok",
467+
headers={"content-type": "text/plain"},
468+
)
469+
470+
async def aclose(self) -> None:
471+
nonlocal closed
472+
closed += 1
473+
474+
monkeypatch.setattr("veadk.cli.frontend_agent_proxy.httpx.AsyncClient", _Client)
475+
app = FastAPI()
476+
mount_agent_surface_proxy_routes(
477+
app,
478+
lambda *_args: SandboxProxyTarget(endpoint="https://sandbox.example/"),
479+
)
480+
481+
with TestClient(app) as client:
482+
first = client.get("/web/hermes/sessions/session-1/surface/token-1/api/first")
483+
second = client.get("/web/hermes/sessions/session-1/surface/token-1/api/second")
484+
485+
assert first.status_code == 200
486+
assert second.status_code == 200
487+
assert created == 1
488+
assert requests == 2
489+
assert closed == 1

tests/cli/test_studio_release.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,41 @@ def _urlopen(url: str, *, timeout: int) -> io.BytesIO:
506506
]
507507

508508

509+
def test_stage_byteplus_dependency_wheels_prefers_primary_pypi(
510+
tmp_path: Path,
511+
monkeypatch: pytest.MonkeyPatch,
512+
) -> None:
513+
content = b"downloaded-wheel"
514+
dependency = StudioDependencyWheel(
515+
filename="downloaded.whl",
516+
url="https://files.pythonhosted.org/packages/example/downloaded.whl",
517+
sha256=hashlib.sha256(content).hexdigest(),
518+
)
519+
urls: list[str] = []
520+
521+
def _urlopen(url: str, *, timeout: int) -> io.BytesIO:
522+
urls.append(url)
523+
assert timeout == 60
524+
return io.BytesIO(content)
525+
526+
monkeypatch.setattr(
527+
"veadk.cli.studio_dependencies.studio_dependency_wheels",
528+
lambda _provider, **_kwargs: (dependency,),
529+
)
530+
monkeypatch.setattr(
531+
"veadk.cli.studio_dependencies.urllib.request.urlopen",
532+
_urlopen,
533+
)
534+
535+
staged = stage_studio_dependency_wheels(
536+
tmp_path / "destination",
537+
provider="byteplus",
538+
)
539+
540+
assert staged[0].read_bytes() == content
541+
assert urls == [dependency.url]
542+
543+
509544
def test_write_dependency_manifest_uses_pinned_wheel_metadata(
510545
tmp_path: Path,
511546
monkeypatch: pytest.MonkeyPatch,

tests/cli/test_studio_sidecar_prerequisites.py

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818

1919
import pytest
2020

21-
from veadk.cli.studio_package import write_studio_package
21+
from veadk.cli.studio_package import (
22+
build_local_studio_requirements,
23+
write_studio_package,
24+
)
2225
from veadk.cli.studio_sidecar_prerequisites import (
2326
DEFAULT_SIDECAR_BASE_IMAGE,
2427
SIDECAR_BASE_IMAGE_ENV,
@@ -52,6 +55,99 @@ def test_write_studio_package_bootstraps_the_preloaded_cli_archive(
5255
assert '--archive "$ROOT_DIR/agentkit-linux-x64.tar.gz"' in run_script
5356

5457

58+
def test_write_studio_update_package_bootstraps_cli_from_remote_artifact(
59+
tmp_path: Path,
60+
) -> None:
61+
package = tmp_path / "package"
62+
63+
write_studio_package(
64+
package,
65+
requirements="veadk-python\n",
66+
site_logo=None,
67+
provider="byteplus",
68+
bundle_agentkit_cli=False,
69+
)
70+
71+
run_script = (package / "run.sh").read_text(encoding="utf-8")
72+
assert "studio_companion --provider byteplus" in run_script
73+
assert "--archive" not in run_script
74+
assert "--runtime-manifest" not in run_script
75+
assert "export VEADK_STUDIO_AGENTKIT_CLI_ARCHIVE=" not in run_script
76+
assert "export VEADK_STUDIO_AGENTKIT_CLI_RUNTIME_MANIFEST=" not in run_script
77+
78+
79+
def test_local_studio_update_skips_full_offline_runtime(
80+
monkeypatch: pytest.MonkeyPatch,
81+
tmp_path: Path,
82+
) -> None:
83+
source_root = tmp_path / "source"
84+
package = tmp_path / "package"
85+
source_root.mkdir()
86+
(source_root / "pyproject.toml").write_text("", encoding="utf-8")
87+
(source_root / "uv.lock").write_text("", encoding="utf-8")
88+
(source_root / "README.md").write_text("", encoding="utf-8")
89+
(source_root / "LICENSE").write_text("", encoding="utf-8")
90+
(source_root / "frontend").mkdir()
91+
(source_root / "frontend" / "package.json").write_text("{}", encoding="utf-8")
92+
(source_root / "frontend" / "package-lock.json").write_text("{}", encoding="utf-8")
93+
(source_root / "veadk").mkdir()
94+
95+
monkeypatch.setattr(
96+
"veadk.cli.studio_package._stage_wheel_source",
97+
lambda _source, _assets, wheel_source: wheel_source.mkdir(parents=True),
98+
)
99+
monkeypatch.setattr(
100+
"veadk.cli.studio_package.shutil.which", lambda _: "/usr/bin/uv"
101+
)
102+
103+
def _build(command: list[str], *, check: bool) -> None:
104+
assert check is True
105+
output_dir = Path(command[-1])
106+
(output_dir / "veadk_python-test-py3-none-any.whl").write_bytes(b"wheel")
107+
108+
monkeypatch.setattr("veadk.cli.studio_package.subprocess.run", _build)
109+
monkeypatch.setattr(
110+
"veadk.cli.studio_package.validate_studio_wheel", lambda *_args: None
111+
)
112+
113+
def _stage_dependencies(destination: Path, **_: object) -> tuple[Path, ...]:
114+
dependency = destination / "dependency-test-py3-none-any.whl"
115+
dependency.write_bytes(b"dependency")
116+
return (dependency,)
117+
118+
monkeypatch.setattr(
119+
"veadk.cli.studio_package.stage_studio_dependency_wheels",
120+
_stage_dependencies,
121+
)
122+
monkeypatch.setattr(
123+
"veadk.cli.studio_package.stage_studio_dependency_sources",
124+
lambda *_args, **_kwargs: pytest.fail("thin update staged source archives"),
125+
)
126+
monkeypatch.setattr(
127+
"veadk.cli.studio_package.stage_studio_agentkit_cli_archive",
128+
lambda *_args, **_kwargs: pytest.fail("thin update staged the CLI archive"),
129+
)
130+
monkeypatch.setattr(
131+
"veadk.cli.studio_package.build_studio_offline_runtime",
132+
lambda *_args, **_kwargs: pytest.fail("thin update built an offline runtime"),
133+
)
134+
135+
requirements = build_local_studio_requirements(
136+
source_root,
137+
package,
138+
provider="byteplus",
139+
offline_runtime=False,
140+
)
141+
142+
assert requirements == (
143+
"./dependency-test-py3-none-any.whl\n./veadk_python-test-py3-none-any.whl\n"
144+
)
145+
assert sorted(path.name for path in package.iterdir()) == [
146+
"dependency-test-py3-none-any.whl",
147+
"veadk_python-test-py3-none-any.whl",
148+
]
149+
150+
55151
def test_project_keeps_native_cli_outside_python_distributions() -> None:
56152
root = Path(__file__).resolve().parents[2]
57153
project = (root / "pyproject.toml").read_text(encoding="utf-8")

tests/cli/test_studio_update.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,9 +449,11 @@ def _build_requirements(
449449
*,
450450
frontend_assets: Path,
451451
provider: str,
452+
offline_runtime: bool,
452453
) -> str:
453454
captured["frontend"] = (frontend_assets / "index.html").read_text()
454455
captured["requirements_provider"] = provider
456+
captured["offline_runtime"] = offline_runtime
455457
return "./veadk.whl\n"
456458

457459
def _write_package(
@@ -460,11 +462,13 @@ def _write_package(
460462
requirements: str,
461463
site_logo: SiteLogo | None,
462464
provider: str = "volcengine",
465+
bundle_agentkit_cli: bool = True,
463466
) -> None:
464467
package_dir.mkdir(parents=True, exist_ok=True)
465468
(package_dir / "run.sh").write_text("run", encoding="utf-8")
466469
captured["requirements"] = requirements
467470
captured["logo"] = site_logo
471+
captured["bundle_agentkit_cli"] = bundle_agentkit_cli
468472

469473
monkeypatch.setattr(
470474
"veadk.cli.studio_package.build_frontend_assets", _build_frontend
@@ -508,8 +512,10 @@ def update_application_code_bundle(self, **kwargs: object) -> str:
508512
assert result.exit_code == 0, result.output
509513
assert captured["frontend"] == "built"
510514
assert captured["requirements_provider"] == "volcengine"
515+
assert captured["offline_runtime"] is False
511516
assert captured["requirements"] == "./veadk.whl\n"
512517
assert captured["logo"] == logo
518+
assert captured["bundle_agentkit_cli"] is False
513519
assert captured["scope"] == {
514520
"access_key": "ak",
515521
"secret_key": "sk",
@@ -745,8 +751,10 @@ def _build_requirements(
745751
*,
746752
frontend_assets: Path,
747753
provider: str,
754+
offline_runtime: bool,
748755
) -> str:
749756
captured["requirements_provider"] = provider
757+
captured["offline_runtime"] = offline_runtime
750758
package_dir.mkdir(parents=True, exist_ok=True)
751759
return "./veadk.whl\n./pydantic.whl\n"
752760

@@ -761,13 +769,18 @@ def _write_package(
761769
requirements: str,
762770
site_logo: SiteLogo | None,
763771
provider: str,
772+
bundle_agentkit_cli: bool,
764773
) -> None:
765774
from veadk.cli.studio_package import studio_run_script
766775

767776
captured["package_requirements"] = requirements
768777
captured["package_logo"] = site_logo
769778
captured["package_provider"] = provider
770-
run_script = studio_run_script(provider=provider) # type: ignore[arg-type]
779+
captured["bundle_agentkit_cli"] = bundle_agentkit_cli
780+
run_script = studio_run_script(
781+
provider=provider, # type: ignore[arg-type]
782+
bundle_agentkit_cli=bundle_agentkit_cli,
783+
)
771784
captured["run_script"] = run_script
772785
package_dir.mkdir(parents=True, exist_ok=True)
773786
(package_dir / "run.sh").write_text(run_script, encoding="utf-8")
@@ -805,7 +818,9 @@ def update_application_code_bundle(self, **kwargs: object) -> str:
805818

806819
assert result.exit_code == 0, result.output
807820
assert captured["requirements_provider"] == "byteplus"
821+
assert captured["offline_runtime"] is False
808822
assert captured["package_provider"] == "byteplus"
823+
assert captured["bundle_agentkit_cli"] is False
809824
assert captured["package_requirements"] == "./veadk.whl\n./pydantic.whl\n"
810825
update = captured["update"]
811826
assert isinstance(update, dict)
@@ -976,6 +991,7 @@ def _write_package(
976991
requirements: str,
977992
site_logo: SiteLogo | None,
978993
provider: str = "volcengine",
994+
bundle_agentkit_cli: bool = True,
979995
) -> None:
980996
package_dir.mkdir(parents=True, exist_ok=True)
981997
captured["logo"] = site_logo

veadk/cli/cli_frontend.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15810,6 +15810,7 @@ def frontend_update(
1581015810
package_dir,
1581115811
frontend_assets=frontend_assets,
1581215812
provider=provider_id,
15813+
offline_runtime=False,
1581315814
)
1581415815
except ValueError as error:
1581515816
raise click.ClickException(str(error)) from error
@@ -15819,6 +15820,7 @@ def frontend_update(
1581915820
requirements=requirements,
1582015821
site_logo=branding_logo,
1582115822
provider=provider_id,
15823+
bundle_agentkit_cli=False,
1582215824
)
1582315825
except ValueError as error:
1582415826
raise click.ClickException(

0 commit comments

Comments
 (0)