fix(desktop): thread eager profile metadata through registry enumeration
Never-interacted remote bots painted as bare handles because roster rows carried only profile names: display_name/title/ui_meta/has_avatar were fetched lazily on first interaction (#91365). Thread credential-free profile metadata from the enumeration-time /api/profiles body through enumerateRegistryAgentSources (main.ts) and buildAgentRoster (connection-registry.ts), keeping it attached to the connection-qualified row across the same-install collapse. The plugin.js botRosterMeta half of the original PR is dropped — superseded by landed #92731. Fixes #91365 Salvaged (partial) from #92708.
This commit is contained in:
@@ -544,6 +544,24 @@ test('roster: unique profiles keep bare handles; duplicates get @name-device', (
|
||||
assert.equal(roster.length, 4)
|
||||
})
|
||||
|
||||
test('roster: source profile metadata follows the connection-qualified row', () => {
|
||||
const local = { id: 'local', kind: 'local' as const, label: 'This device' }
|
||||
const vps = { id: 'vps', kind: 'remote' as const, label: 'VPS', url: 'http://vps:8642' }
|
||||
const vpsMeta = {
|
||||
display_name: 'Emma',
|
||||
ui_meta: { 'hermes-bots': { title: 'Emma', shape: 'blobatar::sun', color: '#8b5cf6' } },
|
||||
has_avatar: true
|
||||
}
|
||||
|
||||
const roster = buildAgentRoster([
|
||||
{ connection: local, profiles: ['default'] },
|
||||
{ connection: vps, profiles: ['default'], profileMetadata: { default: vpsMeta } }
|
||||
])
|
||||
|
||||
assert.deepEqual(roster.find(agent => agent.connectionId === 'vps')?.profileMetadata, vpsMeta)
|
||||
assert.equal(roster.find(agent => agent.connectionId === 'local')?.profileMetadata, undefined)
|
||||
})
|
||||
|
||||
test('rememberSshEnumeration: live list wins, cache then seed default', () => {
|
||||
assert.deepEqual(rememberSshEnumeration({ profiles: ['bob', 'kai'] }, ['stale'], 'ssh'), {
|
||||
profiles: ['bob', 'kai']
|
||||
|
||||
@@ -413,6 +413,9 @@ export interface ConnectionAgents {
|
||||
/** Profile names enumerated from the connection, or null when unreachable /
|
||||
* connect-on-demand (ssh not yet dialed). */
|
||||
profiles: null | string[]
|
||||
/** Credential-free profile metadata from the same connection. Kept separate
|
||||
* from `profiles` so old enumerators can continue returning names only. */
|
||||
profileMetadata?: Record<string, RosterProfileMetadata>
|
||||
/** Present when profiles is null: why enumeration was skipped. */
|
||||
error?: string
|
||||
/** Stable backend identity from the connection's /api/status (`install_id`).
|
||||
@@ -433,6 +436,15 @@ export interface RosterAgent {
|
||||
/** Bare profile name, or `<profile>-<label-slug>` when the profile name
|
||||
* exists on more than one registered source (the @name-device rule). */
|
||||
handle: string
|
||||
/** Rich metadata for this exact connection + profile, when enumerated. */
|
||||
profileMetadata?: RosterProfileMetadata
|
||||
}
|
||||
|
||||
export interface RosterProfileMetadata {
|
||||
display_name?: string
|
||||
title?: string
|
||||
ui_meta?: Record<string, unknown>
|
||||
has_avatar?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -529,18 +541,30 @@ export function buildAgentRoster(
|
||||
// counting names for @name-device disambiguation.
|
||||
const identities = new Map<
|
||||
string,
|
||||
{ connection: RegistryConnection; installId?: string; order: number; profile: string }
|
||||
{
|
||||
connection: RegistryConnection
|
||||
installId?: string
|
||||
order: number
|
||||
profile: string
|
||||
profileMetadata?: RosterProfileMetadata
|
||||
}
|
||||
>()
|
||||
|
||||
let order = 0
|
||||
|
||||
for (const { connection, installId, profiles } of enumerations) {
|
||||
for (const { connection, installId, profiles, profileMetadata } of enumerations) {
|
||||
for (const profile of profiles || []) {
|
||||
const name = String(profile || '').trim() || 'default'
|
||||
const key = `${connection.id}\0${name}`
|
||||
|
||||
if (!identities.has(key)) {
|
||||
identities.set(key, { connection, installId, order, profile: name })
|
||||
identities.set(key, {
|
||||
connection,
|
||||
installId,
|
||||
order,
|
||||
profile: name,
|
||||
...(profileMetadata?.[name] ? { profileMetadata: profileMetadata[name] } : {})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,16 +575,19 @@ export function buildAgentRoster(
|
||||
// are the SAME physical install registered under two addresses, so their
|
||||
// (install, profile) rows are one bot, not two. Connections without an id
|
||||
// (older backends, undialed ssh) keep a per-connection key — no collapse.
|
||||
const backends = new Map<string, { connection: RegistryConnection; order: number; profile: string }[]>()
|
||||
const backends = new Map<
|
||||
string,
|
||||
{ connection: RegistryConnection; order: number; profile: string; profileMetadata?: RosterProfileMetadata }[]
|
||||
>()
|
||||
|
||||
for (const { connection, installId, order: rank, profile } of identities.values()) {
|
||||
for (const { connection, installId, order: rank, profile, profileMetadata } of identities.values()) {
|
||||
const key = installId ? `id:${installId}\0${profile}` : `conn:${connection.id}\0${profile}`
|
||||
const group = backends.get(key)
|
||||
|
||||
if (group) {
|
||||
group.push({ connection, order: rank, profile })
|
||||
group.push({ connection, order: rank, profile, profileMetadata })
|
||||
} else {
|
||||
backends.set(key, [{ connection, order: rank, profile }])
|
||||
backends.set(key, [{ connection, order: rank, profile, profileMetadata }])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,14 +604,15 @@ export function buildAgentRoster(
|
||||
|
||||
const roster: RosterAgent[] = []
|
||||
|
||||
for (const { connection, profile } of rows) {
|
||||
for (const { connection, profile, profileMetadata } of rows) {
|
||||
roster.push({
|
||||
connectionId: connection.id,
|
||||
connectionKind: connection.kind,
|
||||
connectionLabel: connection.label,
|
||||
profile,
|
||||
targetProfile: connection.remoteProfile || profile,
|
||||
handle: agentHandle(profile, connection.label, (counts.get(profile) || 0) > 1)
|
||||
handle: agentHandle(profile, connection.label, (counts.get(profile) || 0) > 1),
|
||||
...(profileMetadata ? { profileMetadata } : {})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,7 @@ import {
|
||||
updateEligibility,
|
||||
upsertConnection
|
||||
} from './connection-registry'
|
||||
import type { RosterProfileMetadata } from './connection-registry'
|
||||
import { describeCrashReason, installCrashForensics } from './crash-forensics'
|
||||
import { adoptServedDashboardToken } from './dashboard-token'
|
||||
import { loadOrCreateInstallationId, sshOwnershipId } from './desktop-installation'
|
||||
@@ -13706,7 +13707,13 @@ async function enumerateRegistryAgentSources(registry = readDesktopConnectionsRe
|
||||
|
||||
return Promise.all(
|
||||
registry.connections.map(async connection => {
|
||||
let raw: { connection: typeof connection; error?: string; installId?: string; profiles: null | string[] }
|
||||
let raw: {
|
||||
connection: typeof connection
|
||||
error?: string
|
||||
installId?: string
|
||||
profiles: null | string[]
|
||||
profileMetadata?: Record<string, RosterProfileMetadata>
|
||||
}
|
||||
|
||||
try {
|
||||
// SSH roster listing must never spawn a dashboard. A stale
|
||||
@@ -13751,13 +13758,52 @@ async function enumerateRegistryAgentSources(registry = readDesktopConnectionsRe
|
||||
? body.profiles.map(p => String(p?.name || '').trim()).filter(Boolean)
|
||||
: []
|
||||
|
||||
const profileMetadata = Array.isArray(body?.profiles)
|
||||
? Object.fromEntries(
|
||||
body.profiles
|
||||
.map(profile => {
|
||||
const name = String(profile?.name || '').trim()
|
||||
|
||||
if (!name) {
|
||||
return null
|
||||
}
|
||||
|
||||
const metadata: RosterProfileMetadata = {}
|
||||
|
||||
if (typeof profile?.display_name === 'string' && profile.display_name.trim()) {
|
||||
metadata.display_name = profile.display_name.trim()
|
||||
}
|
||||
|
||||
if (typeof profile?.title === 'string' && profile.title.trim()) {
|
||||
metadata.title = profile.title.trim()
|
||||
}
|
||||
|
||||
if (profile?.ui_meta && typeof profile.ui_meta === 'object') {
|
||||
metadata.ui_meta = profile.ui_meta
|
||||
}
|
||||
|
||||
if (typeof profile?.has_avatar === 'boolean') {
|
||||
metadata.has_avatar = profile.has_avatar
|
||||
}
|
||||
|
||||
return [name, metadata] as const
|
||||
})
|
||||
.filter((entry): entry is readonly [string, RosterProfileMetadata] => Boolean(entry))
|
||||
)
|
||||
: undefined
|
||||
|
||||
// The root HERMES_HOME is an agent too; enumerations that omit it
|
||||
// (older backends list only named profiles) still get a default row.
|
||||
if (!profiles.includes('default')) {
|
||||
profiles.unshift('default')
|
||||
}
|
||||
|
||||
raw = { connection, profiles, ...(installId ? { installId } : {}) }
|
||||
raw = {
|
||||
connection,
|
||||
profiles,
|
||||
...(installId ? { installId } : {}),
|
||||
...(profileMetadata ? { profileMetadata } : {})
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
raw = { connection, profiles: null, error: String(error?.message || error) }
|
||||
@@ -13769,7 +13815,12 @@ async function enumerateRegistryAgentSources(registry = readDesktopConnectionsRe
|
||||
|
||||
const remembered = rememberSshEnumeration(raw, sshRosterCache.get(connection.id), connection.kind)
|
||||
|
||||
return { connection, ...remembered, ...(raw.installId ? { installId: raw.installId } : {}) }
|
||||
return {
|
||||
connection,
|
||||
...remembered,
|
||||
...(raw.installId ? { installId: raw.installId } : {}),
|
||||
...(raw.profileMetadata ? { profileMetadata: raw.profileMetadata } : {})
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
TomSpoct
|
||||
Reference in New Issue
Block a user