fix(desktop): detect stale backend for projects.create
Probe the projects.* RPC surface, block create with a clear update hint, and avoid the raw "unknown method" toast. Includes i18n for en, zh, ja, and zh-hant. Fixes NousResearch/hermes-agent#54999
This commit is contained in:
parent
217b3c283a
commit
83e10a6777
7 changed files with 80 additions and 13 deletions
|
|
@ -1330,6 +1330,8 @@ export const en: Translations = {
|
|||
copyPath: 'Copy path',
|
||||
removeFromSidebar: 'Hide from sidebar',
|
||||
createFailed: 'Could not create project',
|
||||
staleBackend:
|
||||
'Update the Hermes backend to create projects — your backend is older than this desktop app (Settings → Updates → Backend).',
|
||||
deleteConfirm: 'This removes the saved project from Hermes. Files, git repos, and worktrees stay untouched.',
|
||||
startWork: 'New worktree',
|
||||
newWorktreeTitle: 'New worktree',
|
||||
|
|
|
|||
|
|
@ -1450,6 +1450,8 @@ export const ja = defineLocale({
|
|||
copyPath: 'パスをコピー',
|
||||
removeFromSidebar: 'サイドバーから削除',
|
||||
createFailed: 'プロジェクトを作成できませんでした',
|
||||
staleBackend:
|
||||
'プロジェクトを作成するには Hermes バックエンドを更新してください。バックエンドがこのデスクトップアプリより古いです(設定 → 更新 → バックエンド)。',
|
||||
deleteConfirm:
|
||||
'Hermes から保存済みプロジェクトを削除します。ファイル・git リポジトリ・ワークツリーはそのまま残ります。',
|
||||
startWork: '新しいワークツリー',
|
||||
|
|
|
|||
|
|
@ -1076,6 +1076,7 @@ export interface Translations {
|
|||
copyPath: string
|
||||
removeFromSidebar: string
|
||||
createFailed: string
|
||||
staleBackend: string
|
||||
deleteConfirm: string
|
||||
startWork: string
|
||||
newWorktreeTitle: string
|
||||
|
|
|
|||
|
|
@ -1403,6 +1403,8 @@ export const zhHant = defineLocale({
|
|||
copyPath: '複製路徑',
|
||||
removeFromSidebar: '從側邊欄移除',
|
||||
createFailed: '無法建立專案',
|
||||
staleBackend:
|
||||
'請更新 Hermes 後端以建立專案——目前後端比桌面應用舊(設定 → 更新 → 後端)。',
|
||||
deleteConfirm: '這會從 Hermes 中移除已儲存的專案。檔案、git 儲存庫和工作樹維持不變。',
|
||||
startWork: '新增工作樹',
|
||||
newWorktreeTitle: '新增工作樹',
|
||||
|
|
|
|||
|
|
@ -1510,6 +1510,8 @@ export const zh: Translations = {
|
|||
copyPath: '复制路径',
|
||||
removeFromSidebar: '从侧边栏移除',
|
||||
createFailed: '无法创建项目',
|
||||
staleBackend:
|
||||
'请更新 Hermes 后端以创建项目——当前后端比桌面应用旧(设置 → 更新 → 后端)。',
|
||||
deleteConfirm: '这会从 Hermes 中移除已保存的项目。文件、git 仓库和工作树保持不变。',
|
||||
startWork: '新建工作树',
|
||||
newWorktreeTitle: '新建工作树',
|
||||
|
|
|
|||
6
apps/desktop/src/lib/gateway-rpc.ts
Normal file
6
apps/desktop/src/lib/gateway-rpc.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/** True when a JSON-RPC call failed because the backend predates the method. */
|
||||
export function isMissingRpcMethod(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
|
||||
return /method not found|-32601|unknown method|no such method/i.test(message)
|
||||
}
|
||||
|
|
@ -4,8 +4,11 @@ import { liveSessionProjectId, type SidebarProjectTree } from '@/app/chat/sideba
|
|||
import type { HermesGitBranch } from '@/global'
|
||||
import { desktopDefaultCwd, selectDesktopPaths, writeDesktopFileText } from '@/lib/desktop-fs'
|
||||
import { desktopGit } from '@/lib/desktop-git'
|
||||
import { isMissingRpcMethod } from '@/lib/gateway-rpc'
|
||||
import { persistentAtom } from '@/lib/persisted'
|
||||
import { translateNow } from '@/i18n'
|
||||
import { activeGateway, ensureActiveGatewayOpen } from '@/store/gateway'
|
||||
import { notify } from '@/store/notifications'
|
||||
import { setSidebarAgentsGrouped } from '@/store/layout'
|
||||
import { requestFreshSession } from '@/store/profile'
|
||||
import { $selectedStoredSessionId, $sessions, workspaceCwdForNewSession } from '@/store/session'
|
||||
|
|
@ -26,6 +29,24 @@ export const $activeProjectId = atom<null | string>(null)
|
|||
export const $projectTree = atom<SidebarProjectTree[]>([])
|
||||
export const $projectTreeLoading = atom(false)
|
||||
|
||||
// False when the connected backend predates the projects.* JSON-RPC surface
|
||||
// (same semver label, older install). Null until the first probe.
|
||||
export const $projectsRpcAvailable = atom<boolean | null>(null)
|
||||
|
||||
function markProjectsRpcSuccess(): void {
|
||||
$projectsRpcAvailable.set(true)
|
||||
}
|
||||
|
||||
function markProjectsRpcFailure(err: unknown): void {
|
||||
if (isMissingRpcMethod(err)) {
|
||||
$projectsRpcAvailable.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
function projectsStaleBackendError(): Error {
|
||||
return new Error(translateNow('sidebar.projects.staleBackend'))
|
||||
}
|
||||
|
||||
// Client-side cache eviction (Apollo-style optimistic layer): ids the user just
|
||||
// deleted/archived. The backend tree is a snapshot that still lists them until
|
||||
// its next refresh, so the render-time overlay strips these so the tree matches
|
||||
|
|
@ -216,7 +237,9 @@ function applyPayload(payload: ProjectsPayload): void {
|
|||
export async function refreshProjects(): Promise<void> {
|
||||
try {
|
||||
applyPayload(await gatewayRequest<ProjectsPayload>('projects.list'))
|
||||
} catch {
|
||||
markProjectsRpcSuccess()
|
||||
} catch (err) {
|
||||
markProjectsRpcFailure(err)
|
||||
// Backend may not be ready; keep the last known list.
|
||||
}
|
||||
}
|
||||
|
|
@ -254,7 +277,10 @@ export async function refreshProjectTree(): Promise<void> {
|
|||
$removedSessionIds.set(pending)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
markProjectsRpcSuccess()
|
||||
} catch (err) {
|
||||
markProjectsRpcFailure(err)
|
||||
// Backend may not be ready; keep the last known tree.
|
||||
} finally {
|
||||
$projectTreeLoading.set(false)
|
||||
|
|
@ -410,17 +436,34 @@ function projectInfoToTreeNode(project: ProjectInfo): SidebarProjectTree {
|
|||
}
|
||||
|
||||
export async function createProject(input: CreateProjectInput): Promise<ProjectInfo | null> {
|
||||
const res = await gatewayRequest<{ project: ProjectInfo | null }>('projects.create', {
|
||||
name: input.name,
|
||||
folders: input.folders ?? [],
|
||||
primary_path: input.primaryPath,
|
||||
slug: input.slug,
|
||||
description: input.description,
|
||||
icon: input.icon,
|
||||
color: input.color,
|
||||
board_slug: input.boardSlug,
|
||||
use: input.use ?? false
|
||||
})
|
||||
if ($projectsRpcAvailable.get() === false) {
|
||||
throw projectsStaleBackendError()
|
||||
}
|
||||
|
||||
let res: { project: ProjectInfo | null }
|
||||
|
||||
try {
|
||||
res = await gatewayRequest<{ project: ProjectInfo | null }>('projects.create', {
|
||||
name: input.name,
|
||||
folders: input.folders ?? [],
|
||||
primary_path: input.primaryPath,
|
||||
slug: input.slug,
|
||||
description: input.description,
|
||||
icon: input.icon,
|
||||
color: input.color,
|
||||
board_slug: input.boardSlug,
|
||||
use: input.use ?? false
|
||||
})
|
||||
} catch (err) {
|
||||
if (isMissingRpcMethod(err)) {
|
||||
$projectsRpcAvailable.set(false)
|
||||
throw projectsStaleBackendError()
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
markProjectsRpcSuccess()
|
||||
|
||||
// Not optimistic (the create awaits the RPC first, so there's nothing to roll
|
||||
// back): apply the server's row into the cached list + tree at once, so it
|
||||
|
|
@ -593,6 +636,15 @@ export interface ProjectDialogState {
|
|||
export const $projectDialog = atom<null | ProjectDialogState>(null)
|
||||
|
||||
export function openProjectCreate(): void {
|
||||
if ($projectsRpcAvailable.get() === false) {
|
||||
notify({
|
||||
kind: 'warning',
|
||||
message: translateNow('sidebar.projects.staleBackend')
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
$projectDialog.set({ mode: 'create' })
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue