fix(projects): stop offering a project on a paired server's SSH host, and say where to add it (#29919)

Since #27609 a server serves no SSH target and refuses repo.addRemote, yet the add-project dialog
still offered "Project on this server's SSH host" and the refusal read "SSH connection not found
or not connected". The row is now disabled with a message pointing to adding the SSH host on this
computer, and the server's refusal says the same for older clients that still offer it.
This commit is contained in:
Neil
2026-10-16 19:33:47 -07:00
committed by GitHub
parent 03be5bb9b8
commit 49450b7144
13 changed files with 55 additions and 62 deletions
@@ -635,7 +635,7 @@ describe('OrcaRuntimeService', () => {
.catch((error: unknown) => error)
expect(() => runtime.addSshRepo({ connectionId: 'openclaw' })).toThrow(
'SSH connection "openclaw" not found or not connected'
"This Orca server can't add projects on its SSH hosts."
)
// Why: the defect was a silent local clone/probe recorded as remote, not a bad message,
@@ -1,5 +1,8 @@
import { describe, expect, it, vi } from 'vitest'
import { RuntimeProjectHostSetupController } from './runtime-project-host-setup-controller'
import {
RuntimeProjectHostSetupController,
SERVER_SSH_PROJECT_ADD_UNAVAILABLE_MESSAGE
} from './runtime-project-host-setup-controller'
import { getProjectHostSetupForRepo } from '../../shared/project-host-setup-lookup'
import { projectHostSetupProjectionFromRepos } from '../../shared/project-host-setup-projection'
import type { Repo } from '../../shared/repo-types'
@@ -78,11 +81,11 @@ describe('RuntimeProjectHostSetupController host routing', () => {
expect(addRepo).not.toHaveBeenCalled()
})
it('keeps repo.addRemote failing with the not-connected text', () => {
it('refuses repo.addRemote by saying where the project can be added instead', () => {
const { controller, addRepo } = makeController()
expect(() => controller.addSshRepo({ connectionId: TARGET_ID })).toThrow(
`SSH connection "${TARGET_ID}" not found or not connected`
SERVER_SSH_PROJECT_ADD_UNAVAILABLE_MESSAGE
)
expect(addRepo).not.toHaveBeenCalled()
})
@@ -63,6 +63,9 @@ function assertCloneHostIsSupported(hostId: ExecutionHostId | null | undefined):
)
}
export const SERVER_SSH_PROJECT_ADD_UNAVAILABLE_MESSAGE =
"This Orca server can't add projects on its SSH hosts. Add the SSH host on your computer to open its projects."
function sshTargetNotServedError(connectionId: string): SshHostNotServedError {
return new SshHostNotServedError(connectionId, sshTargetNotConnectedMessage(connectionId))
}
@@ -124,12 +127,15 @@ export class RuntimeProjectHostSetupController {
return this.completeSetup(args, repo, !knownRepoIds.has(repo.id))
}
/** `repo.addRemote` stays registered for older clients; this process serves no SSH target. */
/**
* `repo.addRemote` stays registered for older clients, which still offer it; this process serves
* no SSH target, so the refusal says where such a project can be added instead.
*/
addSshRepo(args: { connectionId: string }): Promise<Repo> {
if (!this.deps.getStore()) {
throw new Error('runtime_unavailable')
}
throw sshTargetNotServedError(args.connectionId)
throw new SshHostNotServedError(args.connectionId, SERVER_SSH_PROJECT_ADD_UNAVAILABLE_MESSAGE)
}
async setupClone(args: ProjectHostSetupCloneArgs): Promise<ProjectHostSetupResult> {
@@ -156,7 +156,7 @@ export function AddRepoDialogStepContent({
hostSelector={hostSelector}
actionsDisabled={actionsDisabled}
browseHostKind={browseHostKind}
serverSshProjects={serverSsh?.state}
offersServerSshHosts={serverSsh !== null}
onBrowse={onBrowse}
onOpenCloneStep={onOpenCloneStep}
onOpenRemoteStep={onOpenRemoteStep}
@@ -140,31 +140,26 @@ describe('AddRepoLocalStartStep', () => {
expect(model.description).toBe('Existing Git repository or folder on this host')
})
it.each([
['supported', false, 'Open a project folder from an SSH host this server connects to'],
['unsupported', true, 'Update this server to add projects on its SSH hosts'],
['unknown', true, 'Open a project folder from an SSH host this server connects to']
] as const)(
"offers a server's own SSH hosts only when the server supports it (%s)",
(serverSshProjects, disabled, description) => {
const { secondaryActions } = getAddRepoLocalStartActions({
browseHostKind: 'runtime',
serverSshProjects,
onBrowse: vi.fn(),
onOpenCloneStep: vi.fn(),
onOpenRemoteStep: vi.fn(),
onOpenCreateStep: vi.fn()
})
const remote = secondaryActions.find((action) => action.kind === 'remote')
it("explains instead of offering a project on a paired server's SSH host", () => {
const { secondaryActions } = getAddRepoLocalStartActions({
browseHostKind: 'runtime',
offersServerSshHosts: true,
onBrowse: vi.fn(),
onOpenCloneStep: vi.fn(),
onOpenRemoteStep: vi.fn(),
onOpenCreateStep: vi.fn()
})
const remote = secondaryActions.find((action) => action.kind === 'remote')
expect(secondaryActions.map((action) => action.kind)).toEqual(['clone', 'remote', 'create'])
expect(remote).toMatchObject({
title: "Project on this server's SSH host",
description,
disabled
})
}
)
expect(secondaryActions.map((action) => action.kind)).toEqual(['clone', 'remote', 'create'])
// Why: the server refuses repo.addRemote, so the row must never open the step that calls it.
expect(remote).toMatchObject({
title: "Project on this server's SSH host",
description:
'Not available from a server. Add the SSH host on this computer to open its projects.',
disabled: true
})
})
it('focuses Browse folder when the default Add Project step opens', async () => {
const { container, root } = await renderLocalStartStepDom()
@@ -68,7 +68,7 @@ type AddRepoLocalStartStepProps = {
hostSelector?: ReactNode
actionsDisabled?: boolean
browseHostKind?: 'local' | 'runtime'
serverSshProjects?: 'supported' | 'unsupported' | 'unknown'
offersServerSshHosts?: boolean
onBrowse: () => void
onOpenCloneStep: () => void
onOpenRemoteStep: () => void
@@ -85,7 +85,7 @@ export function AddRepoLocalStartStep({
hostSelector,
actionsDisabled = false,
browseHostKind = 'local',
serverSshProjects,
offersServerSshHosts,
onBrowse,
onOpenCloneStep,
onOpenRemoteStep,
@@ -101,7 +101,7 @@ export function AddRepoLocalStartStep({
onOpenRemoteStep,
onOpenCreateStep,
browseHostKind,
serverSshProjects
offersServerSshHosts
})
// The white fill + ⏎ chip is a roving selection indicator, not a fixed "primary" badge:
@@ -8,8 +8,8 @@ export type AddRepoLocalStartActionHandlers = {
onOpenRemoteStep: () => void
onOpenCreateStep: () => void
browseHostKind?: 'local' | 'runtime'
/** A paired server's own SSH hosts; `unsupported` when it predates managing them. */
serverSshProjects?: 'supported' | 'unsupported' | 'unknown'
/** A paired server is selected, whose own SSH hosts the dialog explains instead of offering. */
offersServerSshHosts?: boolean
}
export type AddRepoLocalStartAction = {
@@ -27,7 +27,7 @@ export function getAddRepoLocalStartActions({
onOpenRemoteStep,
onOpenCreateStep,
browseHostKind = 'local',
serverSshProjects
offersServerSshHosts = false
}: AddRepoLocalStartActionHandlers): {
primaryAction: AddRepoLocalStartAction
secondaryActions: AddRepoLocalStartAction[]
@@ -52,7 +52,9 @@ export function getAddRepoLocalStartActions({
onClick: onBrowse
}
const remote = serverSshProjects
// Why disabled: a server registers no project on its SSH hosts (each runs its own managed
// server), so the row only says where that project can be added instead.
const remote = offersServerSshHosts
? {
kind: 'remote' as const,
icon: Monitor,
@@ -60,18 +62,11 @@ export function getAddRepoLocalStartActions({
'auto.components.sidebar.add.repo.local.start.actions.serverSshTitle',
"Project on this server's SSH host"
),
description:
serverSshProjects === 'unsupported'
? translate(
'auto.components.sidebar.add.repo.local.start.actions.serverSshUpdate',
'Update this server to add projects on its SSH hosts'
)
: translate(
'auto.components.sidebar.add.repo.local.start.actions.serverSshDescription',
'Open a project folder from an SSH host this server connects to'
),
// Why: an old server has no way to do this, and this client must not do it locally.
disabled: serverSshProjects !== 'supported',
description: translate(
'auto.components.sidebar.add.repo.local.start.actions.serverSshAddOnThisComputer',
'Not available from a server. Add the SSH host on this computer to open its projects.'
),
disabled: true,
onClick: onOpenRemoteStep
}
: null
+1 -2
View File
@@ -6227,8 +6227,7 @@
"7edb8ebe24": "Clone from URL",
"a6c20dca96": "Open a project from an SSH target",
"serverSshTitle": "Project on this server's SSH host",
"serverSshDescription": "Open a project folder from an SSH host this server connects to",
"serverSshUpdate": "Update this server to add projects on its SSH hosts",
"serverSshAddOnThisComputer": "Not available from a server. Add the SSH host on this computer to open its projects.",
"fb4fc5380e": "Local project, Git repo, or folder with many repos",
"2281fdc8c7": "Browse folder",
"runtimeBrowseDescription": "Existing Git repository or folder on this host"
+1 -2
View File
@@ -5990,8 +5990,7 @@
"7edb8ebe24": "Clonar desde URL",
"a6c20dca96": "Abrir un proyecto desde un destino SSH",
"serverSshTitle": "Proyecto en un host SSH de este servidor",
"serverSshDescription": "Abre una carpeta de proyecto de un host SSH al que se conecta este servidor",
"serverSshUpdate": "Actualiza este servidor para añadir proyectos en sus hosts SSH",
"serverSshAddOnThisComputer": "No disponible desde un servidor. Añade el host SSH en este ordenador para abrir sus proyectos.",
"fb4fc5380e": "Proyecto local, repositorio de Git o carpeta con muchos repositorios",
"2281fdc8c7": "Explorar carpeta",
"runtimeBrowseDescription": "Repositorio Git o carpeta existente en este host"
+1 -2
View File
@@ -6103,8 +6103,7 @@
"7edb8ebe24": "Cloner depuis une URL",
"a6c20dca96": "Ouvrir un projet depuis une cible SSH",
"serverSshTitle": "Projet sur un hôte SSH de ce serveur",
"serverSshDescription": "Ouvrir un dossier de projet depuis un hôte SSH auquel ce serveur se connecte",
"serverSshUpdate": "Mettez ce serveur à jour pour ajouter des projets sur ses hôtes SSH",
"serverSshAddOnThisComputer": "Indisponible depuis un serveur. Ajoutez l’hôte SSH sur cet ordinateur pour ouvrir ses projets.",
"fb4fc5380e": "Projet local, dépôt Git ou dossier contenant plusieurs dépôts",
"2281fdc8c7": "Parcourir le dossier",
"runtimeBrowseDescription": "Dépôt Git existant ou dossier sur cette machine"
+1 -2
View File
@@ -5975,8 +5975,7 @@
"7edb8ebe24": "URL からクローン",
"a6c20dca96": "SSH ターゲットからプロジェクトを開く",
"serverSshTitle": "このサーバーの SSH ホスト上のプロジェクト",
"serverSshDescription": "このサーバーが接続する SSH ホストのプロジェクトフォルダーを開きます",
"serverSshUpdate": "SSH ホストにプロジェクトを追加するには、このサーバーを更新してください",
"serverSshAddOnThisComputer": "サーバーからは利用できません。プロジェクトを開くには、このコンピューターに SSH ホストを追加してください。",
"fb4fc5380e": "ローカルプロジェクト、Git リポジトリ、または多数のリポジトリを含むフォルダー",
"2281fdc8c7": "フォルダーを参照",
"runtimeBrowseDescription": "このホスト上の既存の Git リポジトリまたはフォルダー"
+1 -2
View File
@@ -5975,8 +5975,7 @@
"7edb8ebe24": "URL에서 복제",
"a6c20dca96": "SSH 대상에서 프로젝트 열기",
"serverSshTitle": "이 서버의 SSH 호스트에 있는 프로젝트",
"serverSshDescription": "이 서버가 연결하는 SSH 호스트의 프로젝트 폴더를 엽니다",
"serverSshUpdate": "SSH 호스트에 프로젝트를 추가하려면 이 서버를 업데이트하세요",
"serverSshAddOnThisComputer": "서버에서는 사용할 수 없습니다. 프로젝트를 열려면 이 컴퓨터에 SSH 호스트를 추가하세요.",
"fb4fc5380e": "로컬 프로젝트, Git repo 또는 repos가 많은 폴더",
"2281fdc8c7": "폴더 찾아보기",
"runtimeBrowseDescription": "이 호스트의 기존 Git 리포지토리 또는 폴더"
+1 -2
View File
@@ -5975,8 +5975,7 @@
"7edb8ebe24": "从 URL 克隆",
"a6c20dca96": "从 SSH 目标打开项目",
"serverSshTitle": "此服务器 SSH 主机上的项目",
"serverSshDescription": "从此服务器连接的 SSH 主机打开项目文件夹",
"serverSshUpdate": "更新此服务器后才能在其 SSH 主机上添加项目",
"serverSshAddOnThisComputer": "无法从服务器使用。请在此电脑上添加该 SSH 主机以打开其项目。",
"fb4fc5380e": "本地项目、Git 仓库或包含多个仓库的文件夹",
"2281fdc8c7": "浏览文件夹",
"runtimeBrowseDescription": "此主机上的现有 Git 仓库或文件夹"