diff --git a/server/app/domains/remote_control/service/remote_control_service.py b/server/app/domains/remote_control/service/remote_control_service.py
index 8fb54d9e..bf6590dc 100644
--- a/server/app/domains/remote_control/service/remote_control_service.py
+++ b/server/app/domains/remote_control/service/remote_control_service.py
@@ -324,25 +324,40 @@ class RemoteControlService:
return RemoteControlService._get_owned_space(user_id, history.space_id, db)
return None
+ @staticmethod
+ def _ensure_remote_control_supported_space(space: Space) -> None:
+ if space.source_type == SpaceSourceType.LEGACY:
+ raise HTTPException(
+ status_code=400,
+ detail={
+ "code": "REMOTE_CONTROL_LEGACY_SPACE_UNSUPPORTED",
+ "message": "Legacy Spaces do not support remote control.",
+ },
+ )
+
@staticmethod
def _session_space(session: RemoteControlSession, user_id: int, db: Session) -> Space:
if session.space_id:
- return RemoteControlService._get_owned_space(user_id, session.space_id, db)
+ space = RemoteControlService._get_owned_space(user_id, session.space_id, db)
+ RemoteControlService._ensure_remote_control_supported_space(space)
+ return space
project_id, _, _, _ = RemoteControlService._effective_target(session)
if project_id:
space = RemoteControlService._space_for_project(user_id, project_id, db)
if space:
+ RemoteControlService._ensure_remote_control_supported_space(space)
session.space_id = space.id
session.space_name_snapshot = space.name
db.add(session)
db.flush()
return space
- space = SpaceService.ensure_legacy_space(user_id, db)
- session.space_id = space.id
- session.space_name_snapshot = space.name
- db.add(session)
- db.flush()
- return space
+ raise HTTPException(
+ status_code=400,
+ detail={
+ "code": "REMOTE_CONTROL_SPACE_REQUIRED",
+ "message": "Remote control requires a non-legacy Space.",
+ },
+ )
@staticmethod
def _ensure_folder_space(space: Space) -> None:
@@ -515,9 +530,22 @@ class RemoteControlService:
elif target_project_id:
space = RemoteControlService._space_for_project(user_id, target_project_id, db)
if not space:
- space = SpaceService.ensure_legacy_space(user_id, db)
+ raise HTTPException(
+ status_code=400,
+ detail={
+ "code": "REMOTE_CONTROL_SPACE_REQUIRED",
+ "message": "Remote control requires a non-legacy Space.",
+ },
+ )
else:
- space = SpaceService.ensure_legacy_space(user_id, db)
+ raise HTTPException(
+ status_code=400,
+ detail={
+ "code": "REMOTE_CONTROL_SPACE_REQUIRED",
+ "message": "Remote control requires a non-legacy Space.",
+ },
+ )
+ RemoteControlService._ensure_remote_control_supported_space(space)
if target_project_id:
project = RemoteControlService._get_owned_project(user_id, target_project_id, db)
if project and project.space_id != space.id:
diff --git a/server/app/domains/space/service/space_service.py b/server/app/domains/space/service/space_service.py
index e6aac4a1..79ba47cd 100644
--- a/server/app/domains/space/service/space_service.py
+++ b/server/app/domains/space/service/space_service.py
@@ -29,6 +29,7 @@ from app.model.project import (
Project,
ProjectIn,
ProjectOut,
+ ProjectStatus,
ProjectUpdate,
ProjectWorkdirMode,
)
@@ -506,12 +507,31 @@ class SpaceService:
@staticmethod
def list_spaces(user_id: int | str, s: Session) -> list[SpaceOut]:
canonical_user_id = SpaceService.canonical_user_id(user_id)
- SpaceService.ensure_legacy_space(canonical_user_id, s)
spaces = s.exec(
select(Space)
.where(Space.user_id == canonical_user_id)
.order_by(Space.updated_at.desc())
).all()
+ legacy_space_ids = [
+ space.id
+ for space in spaces
+ if space.source_type == SpaceSourceType.LEGACY
+ ]
+ if legacy_space_ids:
+ legacy_space_ids_with_projects = set(
+ s.exec(
+ select(Project.space_id)
+ .where(Project.user_id == canonical_user_id)
+ .where(Project.space_id.in_(legacy_space_ids))
+ .where(Project.status != ProjectStatus.ARCHIVED)
+ ).all()
+ )
+ spaces = [
+ space
+ for space in spaces
+ if space.source_type != SpaceSourceType.LEGACY
+ or space.id in legacy_space_ids_with_projects
+ ]
return [SpaceOut.from_model(space) for space in spaces]
@staticmethod
diff --git a/server/tests/test_space_legacy_behavior.py b/server/tests/test_space_legacy_behavior.py
new file mode 100644
index 00000000..f0b2522f
--- /dev/null
+++ b/server/tests/test_space_legacy_behavior.py
@@ -0,0 +1,112 @@
+# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
+
+from types import SimpleNamespace
+
+import pytest
+from fastapi import HTTPException
+
+from app.domains.remote_control.service.remote_control_service import RemoteControlService
+from app.domains.space.service.space_service import SpaceService
+from app.model.space.space import SpaceSourceType
+
+
+class _ExecResult:
+ def __init__(self, rows):
+ self._rows = rows
+
+ def all(self):
+ return self._rows
+
+
+class _ListSpacesSession:
+ def __init__(self, rows=None, project_space_ids=None):
+ self._rows = rows or []
+ self._project_space_ids = project_space_ids or []
+ self._exec_count = 0
+
+ def exec(self, _statement):
+ self._exec_count += 1
+ if self._exec_count > 1:
+ return _ExecResult(self._project_space_ids)
+ return _ExecResult(self._rows)
+
+
+def _space(
+ space_id: str,
+ source_type: str,
+ *,
+ user_id: str = "user_new",
+ name: str = "Untitled Space",
+):
+ return SimpleNamespace(
+ id=space_id,
+ user_id=user_id,
+ name=name,
+ description=None,
+ source_type=source_type,
+ root_path=None,
+ root_fingerprint=None,
+ status="active",
+ schema_version=1,
+ metadata_json={"legacy": True} if source_type == SpaceSourceType.LEGACY else None,
+ created_at=None,
+ updated_at=None,
+ )
+
+
+def test_list_spaces_does_not_create_legacy_for_empty_user(monkeypatch):
+ def fail_ensure(*_args, **_kwargs):
+ raise AssertionError("list_spaces must not create Legacy Space")
+
+ monkeypatch.setattr(SpaceService, "ensure_legacy_space", fail_ensure)
+
+ assert SpaceService.list_spaces("user_new", _ListSpacesSession()) == []
+
+
+def test_list_spaces_hides_empty_legacy_space():
+ spaces = [
+ _space("legacy_user_new", SpaceSourceType.LEGACY, name="Legacy Space"),
+ _space("space_new", SpaceSourceType.BLANK),
+ ]
+
+ result = SpaceService.list_spaces("user_new", _ListSpacesSession(spaces))
+
+ assert [space.id for space in result] == ["space_new"]
+
+
+def test_list_spaces_keeps_legacy_space_with_active_projects():
+ legacy_space = _space(
+ "legacy_user_old",
+ SpaceSourceType.LEGACY,
+ user_id="user_old",
+ name="Legacy Space",
+ )
+
+ result = SpaceService.list_spaces(
+ "user_old",
+ _ListSpacesSession([legacy_space], project_space_ids=["legacy_user_old"]),
+ )
+
+ assert [space.id for space in result] == ["legacy_user_old"]
+
+
+def test_remote_control_rejects_legacy_space():
+ with pytest.raises(HTTPException) as exc:
+ RemoteControlService._ensure_remote_control_supported_space(
+ SimpleNamespace(source_type=SpaceSourceType.LEGACY)
+ )
+
+ assert exc.value.status_code == 400
+ assert exc.value.detail["code"] == "REMOTE_CONTROL_LEGACY_SPACE_UNSUPPORTED"
diff --git a/src/components/Dispatch/index.tsx b/src/components/Dispatch/index.tsx
index 731d0b92..a1880d69 100644
--- a/src/components/Dispatch/index.tsx
+++ b/src/components/Dispatch/index.tsx
@@ -26,6 +26,7 @@ import {
revokeRemoteControlSession,
waitForRemoteControlBridgeConnected,
} from '@/lib/remoteControl';
+import { canUseRemoteControlInSpace } from '@/lib/spaceLabel';
import { cn } from '@/lib/utils';
import { getConnectionConfig } from '@/store/connectionStore';
import { useProjectRuntimeStore } from '@/store/projectRuntimeStore';
@@ -176,6 +177,7 @@ interface ChannelRowProps {
icon?: React.ReactNode;
imgSrc?: string;
disabled?: boolean;
+ disabledLabel?: string;
comingSoon?: boolean;
hasActiveSessions?: boolean;
loading?: boolean;
@@ -187,12 +189,14 @@ function ChannelRow({
icon,
imgSrc,
disabled,
+ disabledLabel,
comingSoon,
hasActiveSessions,
loading,
onStart,
}: ChannelRowProps) {
const { t } = useTranslation();
+ const isDisabled = Boolean(disabled);
return (
@@ -228,13 +232,19 @@ function ChannelRow({
)}
- {!disabled && !comingSoon && hasActiveSessions && (
+ {isDisabled && !comingSoon && disabledLabel && (
+
+ {disabledLabel}
+
+ )}
+
+ {!isDisabled && !comingSoon && hasActiveSessions && (
{t('layout.dispatch-connected', { defaultValue: 'Connected' })}
)}
- {!disabled && !comingSoon && !hasActiveSessions && (
+ {!isDisabled && !comingSoon && !hasActiveSessions && (