fix(desktop): one scoped-dial priority helper; Capabilities selector dials foreground too
Fold the salvaged per-call ternaries in api/config.ts into a single scopedDialPriority(scope) helper on api/client.ts and apply it to the Capabilities scope selector's cold-start reads (getSkills / getToolsets / getMcpCatalog), which hit the same background-capped pool queue when the selector targets a stopped profile. Drop the salvaged main.ts source-text change-detector test; keep only the string update the existing #90812 wiring test needs. The renderer seam test (hermes-capability-scope) is the invariant: an explicit scope carries priority 'foreground', the ambient path stays untagged. Part of #111651 (salvage #111672)
This commit is contained in:
@@ -135,7 +135,7 @@ describe('main.ts wiring for #90812', () => {
|
||||
it('routes the registry-scoped dial IPC through the claim keyed by backendScopeKey(connectionId, profile)', () => {
|
||||
const handlerStart = mainSource.indexOf("ipcMain.handle('hermes:connection:for', ")
|
||||
expect(handlerStart).toBeGreaterThan(-1)
|
||||
const body = mainSource.slice(handlerStart, handlerStart + 1_600)
|
||||
const body = mainSource.slice(handlerStart, handlerStart + 1_200)
|
||||
|
||||
expect(body).toContain('const scopeKey = backendScopeKey(id, profile)')
|
||||
expect(body).toContain('backendDialClaims.run(scopeKey, ')
|
||||
@@ -162,7 +162,7 @@ describe('main.ts wiring for #90812', () => {
|
||||
it('routes a terminal-pane backend resolve through the single-owner claim on both the registry and local branches', () => {
|
||||
const handlerStart = mainSource.indexOf('async function ensureTerminalBackend(webContentsId: number) {')
|
||||
expect(handlerStart).toBeGreaterThan(-1)
|
||||
const body = mainSource.slice(handlerStart, handlerStart + 1_200)
|
||||
const body = mainSource.slice(handlerStart, handlerStart + 900)
|
||||
|
||||
expect(body).toContain('backendDialClaims.run(backendScopeKey(windowRoute.connectionId, windowRoute.profile)')
|
||||
expect(body).toContain('ensureRegistryBackend(windowRoute.connectionId, windowRoute.profile)')
|
||||
@@ -195,21 +195,9 @@ describe('main.ts wiring for #90812', () => {
|
||||
it('routes every registry-scoped REST dispatch (hermes:api) through the single-owner claim', () => {
|
||||
const handlerStart = mainSource.indexOf('async function dispatchRegistryApiRequest(')
|
||||
expect(handlerStart).toBeGreaterThan(-1)
|
||||
const body = mainSource.slice(handlerStart, handlerStart + 1_600)
|
||||
const body = mainSource.slice(handlerStart, handlerStart + 1_000)
|
||||
|
||||
expect(body).toContain('backendDialClaims.run(backendScopeKey(registryConnectionId, routeProfile)')
|
||||
expect(body).toContain("ensureRegistryBackend(registryConnectionId, routeProfile, '', { spawnPriority })")
|
||||
})
|
||||
|
||||
it('forwards foreground API intent through both local and registry backend resolution', () => {
|
||||
const registryStart = mainSource.indexOf('async function dispatchRegistryApiRequest(')
|
||||
const registryBody = mainSource.slice(registryStart, registryStart + 1_300)
|
||||
const apiStart = mainSource.indexOf('async function handleHermesApiRequest(request)')
|
||||
const apiBody = mainSource.slice(apiStart, apiStart + 3_500)
|
||||
|
||||
expect(registryBody).toContain('const spawnPriority = spawnPriorityFrom(request?.priority)')
|
||||
expect(registryBody).toContain("ensureRegistryBackend(registryConnectionId, routeProfile, '', { spawnPriority })")
|
||||
expect(apiBody).toContain('const spawnPriority = spawnPriorityFrom(request?.priority)')
|
||||
expect(apiBody).toContain('ensureBackend(routeProfile, { passive: request?.passive, spawnPriority })')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -155,6 +155,15 @@ export function capabilityScoped(scope?: ProfileScope): { connectionId?: string;
|
||||
return { ...profileScoped(scope), ...connectionScoped() }
|
||||
}
|
||||
|
||||
/** Spawn priority for a REST call that may cold-start a pooled backend. An
|
||||
* explicit scope is a user pointing a scope selector (Settings "Applies to",
|
||||
* Capabilities) at another profile — a visible action that may take the
|
||||
* pool's reserved foreground slot (#111651). The ambient path stays untagged,
|
||||
* main's background default, so hydration cannot consume that slot. */
|
||||
export function scopedDialPriority(scope?: ProfileScope): { priority?: 'foreground' } {
|
||||
return scope == null ? {} : { priority: 'foreground' }
|
||||
}
|
||||
|
||||
/** Stable cache-key for a capability scope: `profile` for the ambient/legacy
|
||||
* path, `connectionId::profile` for ANY explicit pin — `local` included. An
|
||||
* explicit "This device" pick and the ambient path are no longer guaranteed
|
||||
|
||||
@@ -14,14 +14,14 @@ import type {
|
||||
StatusResponse
|
||||
} from '@/types/hermes'
|
||||
|
||||
import { capabilityScoped, hermesApi, type ProfileScope, profileScoped, STARTUP_REQUEST_TIMEOUT_MS } from './client'
|
||||
|
||||
// Settings' explicit "Applies to" selection is a visible user action. Only
|
||||
// tag concrete profile scopes: ambient reads remain background so hydration
|
||||
// and unrelated config consumers cannot consume foreground pool capacity.
|
||||
function settingsScopePriority(profile?: ProfileScope): { priority?: 'foreground' } {
|
||||
return profile == null ? {} : { priority: 'foreground' }
|
||||
}
|
||||
import {
|
||||
capabilityScoped,
|
||||
hermesApi,
|
||||
type ProfileScope,
|
||||
profileScoped,
|
||||
scopedDialPriority,
|
||||
STARTUP_REQUEST_TIMEOUT_MS
|
||||
} from './client'
|
||||
|
||||
export function getStatus(): Promise<StatusResponse> {
|
||||
return hermesApi<StatusResponse>({
|
||||
@@ -81,7 +81,7 @@ export function getHermesConfigRecord(
|
||||
): Promise<HermesConfigRecord> {
|
||||
return window.hermesDesktop.api<HermesConfigRecord>({
|
||||
...capabilityScoped(profile),
|
||||
...settingsScopePriority(profile),
|
||||
...scopedDialPriority(profile),
|
||||
path: includeDefaults ? '/api/config' : '/api/config?include_defaults=false'
|
||||
})
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export function getHermesConfigDefaults(): Promise<HermesConfigRecord> {
|
||||
export function getHermesConfigSchema(profile?: null | string): Promise<ConfigSchemaResponse> {
|
||||
return hermesApi<ConfigSchemaResponse>({
|
||||
...profileScoped(profile),
|
||||
...(profile == null ? {} : { priority: 'foreground' as const }),
|
||||
...scopedDialPriority(profile),
|
||||
path: '/api/config/schema'
|
||||
})
|
||||
}
|
||||
@@ -109,7 +109,7 @@ export function saveHermesConfig(
|
||||
): Promise<{ ok: boolean }> {
|
||||
return hermesApi<{ ok: boolean }>({
|
||||
...profileScoped(profile),
|
||||
...(profile == null ? {} : { priority: 'foreground' as const }),
|
||||
...scopedDialPriority(profile),
|
||||
path: preserveLanguage ? '/api/config?preserve_language=true' : '/api/config',
|
||||
method: 'PUT',
|
||||
body: { config }
|
||||
@@ -122,7 +122,7 @@ export function saveHermesConfig(
|
||||
export function saveHermesConfigRecord(config: HermesConfigRecord, profile?: ProfileScope): Promise<{ ok: boolean }> {
|
||||
return window.hermesDesktop.api<{ ok: boolean }>({
|
||||
...capabilityScoped(profile),
|
||||
...settingsScopePriority(profile),
|
||||
...scopedDialPriority(profile),
|
||||
path: '/api/config',
|
||||
method: 'PUT',
|
||||
body: { config }
|
||||
@@ -132,7 +132,7 @@ export function saveHermesConfigRecord(config: HermesConfigRecord, profile?: Pro
|
||||
export function getEnvVars(profile?: null | string): Promise<Record<string, EnvVarInfo>> {
|
||||
return hermesApi<Record<string, EnvVarInfo>>({
|
||||
...profileScoped(profile),
|
||||
...(profile == null ? {} : { priority: 'foreground' as const }),
|
||||
...scopedDialPriority(profile),
|
||||
path: '/api/env'
|
||||
})
|
||||
}
|
||||
@@ -140,7 +140,7 @@ export function getEnvVars(profile?: null | string): Promise<Record<string, EnvV
|
||||
export function setEnvVar(key: string, value: string, profile?: ProfileScope): Promise<{ ok: boolean }> {
|
||||
return window.hermesDesktop.api<{ ok: boolean }>({
|
||||
...capabilityScoped(profile),
|
||||
...settingsScopePriority(profile),
|
||||
...scopedDialPriority(profile),
|
||||
path: '/api/env',
|
||||
method: 'PUT',
|
||||
body: { key, value }
|
||||
@@ -150,7 +150,7 @@ export function setEnvVar(key: string, value: string, profile?: ProfileScope): P
|
||||
export function deleteEnvVar(key: string, profile?: ProfileScope): Promise<{ ok: boolean }> {
|
||||
return window.hermesDesktop.api<{ ok: boolean }>({
|
||||
...capabilityScoped(profile),
|
||||
...settingsScopePriority(profile),
|
||||
...scopedDialPriority(profile),
|
||||
path: '/api/env',
|
||||
method: 'DELETE',
|
||||
body: { key }
|
||||
@@ -160,7 +160,7 @@ export function deleteEnvVar(key: string, profile?: ProfileScope): Promise<{ ok:
|
||||
export function revealEnvVar(key: string, profile?: ProfileScope): Promise<{ key: string; value: string }> {
|
||||
return window.hermesDesktop.api<{ key: string; value: string }>({
|
||||
...capabilityScoped(profile),
|
||||
...settingsScopePriority(profile),
|
||||
...scopedDialPriority(profile),
|
||||
path: '/api/env/reveal',
|
||||
method: 'POST',
|
||||
body: { key }
|
||||
@@ -223,7 +223,7 @@ export function deleteCustomEndpoint(id: string): Promise<CustomEndpointsRespons
|
||||
export function listOAuthProviders(profile?: null | string): Promise<OAuthProvidersResponse> {
|
||||
return hermesApi<OAuthProvidersResponse>({
|
||||
...profileScoped(profile),
|
||||
...(profile == null ? {} : { priority: 'foreground' as const }),
|
||||
...scopedDialPriority(profile),
|
||||
path: '/api/providers/oauth'
|
||||
})
|
||||
}
|
||||
@@ -234,7 +234,7 @@ export function disconnectOAuthProvider(
|
||||
): Promise<{ ok: boolean; provider: string }> {
|
||||
return hermesApi<{ ok: boolean; provider: string }>({
|
||||
...profileScoped(profile),
|
||||
...(profile == null ? {} : { priority: 'foreground' as const }),
|
||||
...scopedDialPriority(profile),
|
||||
path: `/api/providers/oauth/${encodeURIComponent(providerId)}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
@@ -243,7 +243,7 @@ export function disconnectOAuthProvider(
|
||||
export function startOAuthLogin(providerId: string, profile?: ProfileScope): Promise<OAuthStartResponse> {
|
||||
return window.hermesDesktop.api<OAuthStartResponse>({
|
||||
...capabilityScoped(profile),
|
||||
...settingsScopePriority(profile),
|
||||
...scopedDialPriority(profile),
|
||||
path: `/api/providers/oauth/${encodeURIComponent(providerId)}/start`,
|
||||
method: 'POST',
|
||||
body: {}
|
||||
@@ -258,7 +258,7 @@ export function submitOAuthCode(
|
||||
): Promise<OAuthSubmitResponse> {
|
||||
return hermesApi<OAuthSubmitResponse>({
|
||||
...profileScoped(profile),
|
||||
...(profile == null ? {} : { priority: 'foreground' as const }),
|
||||
...scopedDialPriority(profile),
|
||||
path: `/api/providers/oauth/${encodeURIComponent(providerId)}/submit`,
|
||||
method: 'POST',
|
||||
body: { session_id: sessionId, code }
|
||||
@@ -272,7 +272,7 @@ export function pollOAuthSession(
|
||||
): Promise<OAuthPollResponse> {
|
||||
return window.hermesDesktop.api<OAuthPollResponse>({
|
||||
...capabilityScoped(profile),
|
||||
...settingsScopePriority(profile),
|
||||
...scopedDialPriority(profile),
|
||||
path: `/api/providers/oauth/${encodeURIComponent(providerId)}/poll/${encodeURIComponent(sessionId)}`
|
||||
})
|
||||
}
|
||||
@@ -280,7 +280,7 @@ export function pollOAuthSession(
|
||||
export function cancelOAuthSession(sessionId: string, profile?: null | string): Promise<{ ok: boolean }> {
|
||||
return hermesApi<{ ok: boolean }>({
|
||||
...profileScoped(profile),
|
||||
...(profile == null ? {} : { priority: 'foreground' as const }),
|
||||
...scopedDialPriority(profile),
|
||||
path: `/api/providers/oauth/sessions/${encodeURIComponent(sessionId)}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { McpCatalogResponse, McpServerSummary } from '@/types/hermes'
|
||||
|
||||
import { capabilityScoped, hermesApi, type ProfileScope, profileScoped } from './client'
|
||||
import { capabilityScoped, hermesApi, type ProfileScope, profileScoped, scopedDialPriority } from './client'
|
||||
|
||||
export interface McpTestResult {
|
||||
ok: boolean
|
||||
@@ -117,6 +117,7 @@ export function setMcpServerEnabled(name: string, enabled: boolean): Promise<{ o
|
||||
export function getMcpCatalog(profile?: ProfileScope): Promise<McpCatalogResponse> {
|
||||
return window.hermesDesktop.api<McpCatalogResponse>({
|
||||
...capabilityScoped(profile),
|
||||
...scopedDialPriority(profile),
|
||||
path: '/api/mcp/catalog'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,11 +9,12 @@ import type {
|
||||
} from '@/types/hermes'
|
||||
import type { ActionResponse } from '@/types/hermes'
|
||||
|
||||
import { capabilityScoped, hermesApi, type ProfileScope, profileScoped } from './client'
|
||||
import { capabilityScoped, hermesApi, type ProfileScope, profileScoped, scopedDialPriority } from './client'
|
||||
|
||||
export function getSkills(profile?: ProfileScope): Promise<SkillInfo[]> {
|
||||
return window.hermesDesktop.api<SkillInfo[]>({
|
||||
...capabilityScoped(profile),
|
||||
...scopedDialPriority(profile),
|
||||
path: '/api/skills'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
ToolsetModelsResponse
|
||||
} from '@/types/hermes'
|
||||
|
||||
import { capabilityScoped, hermesApi, type ProfileScope, profileScoped } from './client'
|
||||
import { capabilityScoped, hermesApi, type ProfileScope, profileScoped, scopedDialPriority } from './client'
|
||||
|
||||
// The optional trailing `profile` on every capability fetcher below is the
|
||||
// Capabilities view's profile-scope override: it lets the Skills/Tools/MCP
|
||||
@@ -17,6 +17,7 @@ import { capabilityScoped, hermesApi, type ProfileScope, profileScoped } from '.
|
||||
export function getToolsets(profile?: ProfileScope): Promise<ToolsetInfo[]> {
|
||||
return window.hermesDesktop.api<ToolsetInfo[]>({
|
||||
...capabilityScoped(profile),
|
||||
...scopedDialPriority(profile),
|
||||
path: '/api/tools/toolsets'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -74,9 +74,13 @@ describe('capability helpers are connection-scoped', () => {
|
||||
expect(last().connectionId).toBe('gw-tailscale')
|
||||
})
|
||||
|
||||
it('marks an explicitly scoped Settings config read as foreground', () => {
|
||||
it('marks an explicitly scoped Settings / Capabilities read as foreground (#111651)', () => {
|
||||
// A scope-selector pick is a visible user action: its cold dial must take
|
||||
// the pool's reserved foreground slot instead of queueing behind hydration.
|
||||
getHermesConfigRecord('coder')
|
||||
expect(last()).toMatchObject({ profile: 'coder', priority: 'foreground' })
|
||||
|
||||
void getSkills('coder')
|
||||
expect(last()).toMatchObject({ profile: 'coder', priority: 'foreground' })
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user