fix(desktop): hide unreachable same-name Bot Mode roster twins

Group rooms persist source-qualified members. After Desktop switches to
the built-in This-device source, a dead loopback row still listed next
to the live profile and looked like a second agent. Collapse only the
sidebar tiles; $lastRoster, group seats, and mentions keep every
(connectionId, profile) identity.
This commit is contained in:
686f6c61
2026-08-26 09:50:02 +02:00
committed by Teknium
parent ac2ec56833
commit 21054b751e
3 changed files with 209 additions and 2 deletions
+26 -1
View File
@@ -5242,6 +5242,31 @@ function botSourceStatus(bot) {
return { available: true, key: 'unknown', label: 'Status unknown', tone: 'muted' }
}
/** Drop unreachable same-name copies from the top-level agent list.
*
* Group rooms persist source-qualified members. After Desktop moves to the
* built-in This-device source, the old loopback row (127.0.0.1:port, not
* listening) still sits next to the live profile and looks like a second
* agent (#92286). Routing identities stay intact: this only filters sidebar
* tiles. $lastRoster, group members, and @-mentions still see every
* (connectionId, profile) row.
*
* Ghosts stay: a selected-but-offline owner must remain visible rather than
* being replaced by a same-named twin on another gateway. A name with no
* reachable copy is kept, so a genuinely down source still has a row. */
function preferReachableSameNameRows(bots) {
const rows = Array.isArray(bots) ? bots : []
const reachableNames = new Set()
for (const bot of rows) {
if (botSourceStatus(bot).available) {
reachableNames.add(bot?.name)
}
}
return rows.filter(bot => botSourceStatus(bot).available || bot?.ghost || !reachableNames.has(bot?.name))
}
// ── cross-connection routing ─────────────────────────────────────────────────
// A bot from another registered connection (remoteSource rows) is reached
// through host.requestProfile with a route descriptor; local bots keep the
@@ -14498,7 +14523,7 @@ function BotsPane() {
const botRows =
rowKindFilter === 'groups'
? []
: filteredRoster.map(bot => ({
: preferReachableSameNameRows(filteredRoster).map(bot => ({
kind: 'bot',
bot,
pinned: isPinned(bot),
@@ -32,7 +32,7 @@ function runtime() {
.replace(/^import .* from 'react\/jsx-runtime'\r?\n/m, '')
.replace('export default {', 'globalThis.plugin = {')
.concat(
'\nglobalThis.__mergeMultiSourceRoster = mergeMultiSourceRoster;\nglobalThis.__botHandle = botHandle;\nglobalThis.__botRosterKey = botRosterKey;\nglobalThis.__botRosterMeta = botRosterMeta;\nglobalThis.__displayName = displayName;\nglobalThis.__filterBots = filterBots;\nglobalThis.__resolveRosterMentions = resolveRosterMentions;\nglobalThis.__botConnectionRoute = botConnectionRoute;\nglobalThis.__resolveBotConnectionRoute = resolveBotConnectionRoute;'
'\nglobalThis.__mergeMultiSourceRoster = mergeMultiSourceRoster;\nglobalThis.__botHandle = botHandle;\nglobalThis.__botRosterKey = botRosterKey;\nglobalThis.__botRosterMeta = botRosterMeta;\nglobalThis.__displayName = displayName;\nglobalThis.__filterBots = filterBots;\nglobalThis.__resolveRosterMentions = resolveRosterMentions;\nglobalThis.__botConnectionRoute = botConnectionRoute;\nglobalThis.__resolveBotConnectionRoute = resolveBotConnectionRoute;\nglobalThis.__preferReachableSameNameRows = preferReachableSameNameRows;'
)
vm.runInNewContext(code, context)
return context
@@ -716,3 +716,153 @@ test('source contract: active roster queries use the SDK ambient owner route', (
'roster hydration and the session sweep must both use the upstream ambient-owner route'
)
})
test('preferReachableSameNameRows: drops a dead loopback twin when This device is live', () => {
const { __preferReachableSameNameRows: prefer } = runtime()
const live = {
name: 'profile-a',
handle: 'profile-a-this-device',
connectionId: 'local',
connectionKind: 'local',
connectionLabel: 'This device',
sourceReachable: true
}
const dead = {
name: 'profile-a',
handle: 'profile-a-127-0-0-1-19119',
connectionId: 'loopback-19119',
connectionKind: 'remote',
connectionLabel: '127.0.0.1:19119',
remoteSource: true,
sourceScoped: true,
sourceReachable: false
}
const other = {
name: 'profile-b',
handle: 'profile-b-127-0-0-1-19119',
connectionId: 'loopback-19119',
connectionKind: 'remote',
connectionLabel: '127.0.0.1:19119',
remoteSource: true,
sourceScoped: true,
sourceReachable: false
}
const out = prefer([dead, live, other])
assert.equal(out.length, 2)
assert.equal(out[0], live)
assert.equal(out[1], other)
})
test('preferReachableSameNameRows: keeps two live sources with the same profile name', () => {
const { __preferReachableSameNameRows: prefer } = runtime()
const local = {
name: 'research',
connectionId: 'local',
connectionLabel: 'This device',
sourceReachable: true
}
const homelab = {
name: 'research',
connectionId: 'homelab',
connectionLabel: 'Homelab',
remoteSource: true,
sourceReachable: true
}
const out = prefer([local, homelab])
assert.equal(out.length, 2)
assert.equal(out[0], local)
assert.equal(out[1], homelab)
})
test('preferReachableSameNameRows: keeps an unreachable row when no live twin exists', () => {
const { __preferReachableSameNameRows: prefer } = runtime()
const dead = {
name: 'research',
connectionId: 'loopback-19119',
sourceReachable: false
}
const out = prefer([dead])
assert.equal(out.length, 1)
assert.equal(out[0], dead)
})
test('preferReachableSameNameRows: connect-on-demand counts as reachable', () => {
const { __preferReachableSameNameRows: prefer } = runtime()
const onDemand = {
name: 'research',
connectionId: 'mac-mini',
sourceError: 'connect-on-demand',
sourceReachable: false
}
const dead = {
name: 'research',
connectionId: 'loopback-19119',
sourceReachable: false
}
const out = prefer([onDemand, dead])
assert.equal(out.length, 1)
assert.equal(out[0], onDemand)
})
test('preferReachableSameNameRows: sourceMissing drops when a live same-name row exists', () => {
const { __preferReachableSameNameRows: prefer } = runtime()
const live = { name: 'research', connectionId: 'local', sourceReachable: true }
const missing = {
name: 'research',
connectionId: 'gone',
sourceMissing: true,
sourceReachable: false
}
const out = prefer([missing, live])
assert.equal(out.length, 1)
assert.equal(out[0], live)
})
test('preferReachableSameNameRows: ghosts stay so a selected offline owner is not replaced', () => {
const { __preferReachableSameNameRows: prefer } = runtime()
const live = { name: 'research', connectionId: 'local', sourceReachable: true }
const ghost = {
name: 'research',
connectionId: 'loopback-19119',
ghost: true,
sourceReachable: false
}
const out = prefer([ghost, live])
assert.equal(out.length, 2)
assert.equal(out[0], ghost)
assert.equal(out[1], live)
})
test('preferReachableSameNameRows: does not mutate the input list', () => {
const { __preferReachableSameNameRows: prefer } = runtime()
const rows = [
{ name: 'research', connectionId: 'loopback-19119', sourceReachable: false },
{ name: 'research', connectionId: 'local', sourceReachable: true }
]
prefer(rows)
assert.equal(rows.length, 2)
assert.equal(rows[0].connectionId, 'loopback-19119')
})
test('source contract: presentation collapses unreachable twins; shared roster does not', () => {
assert.match(source, /preferReachableSameNameRows\(filteredRoster\)/)
assert.match(source, /\$lastRoster\.set\(roster\.filter\(row => !row\?\.ghost\)\)/)
assert.doesNotMatch(
source.slice(source.indexOf('function groupChatMemberBots'), source.indexOf('function durableGroupChatMembers')),
/preferReachableSameNameRows/
)
})
@@ -125,6 +125,38 @@ test('groupChatMemberBots: seats local meta members plus stored remote descripto
assert.equal(members[2], roster[2])
})
test('groupChatMemberBots: persisted unreachable source members stay seated next to a live same-name twin', () => {
const { groupChatMemberBots, $groupChats } = load()
const live = {
name: 'profile-a',
handle: 'profile-a-this-device',
connectionId: 'local',
connectionKind: 'local',
connectionLabel: 'This device',
sourceReachable: true
}
const stored = {
name: 'profile-a',
handle: 'profile-a-127-0-0-1-19119',
connectionId: 'loopback-19119',
connectionKind: 'remote',
connectionLabel: '127.0.0.1:19119',
remoteSource: true,
sourceScoped: true,
sourceReachable: false
}
$groupChats.set({ Research: { log: [], members: [stored] } })
const members = groupChatMemberBots('Research', [live, stored], {
'profile-a': { group: 'Research' }
})
assert.equal(members.length, 2)
assert.equal(members[0], live)
assert.equal(members[1], stored)
assert.equal(members[1].handle, 'profile-a-127-0-0-1-19119')
})
test('groupChatMemberBots: stored descriptors beat presentation-only ghosts', () => {
const { groupChatMemberBots, $groupChats } = load()
const ghost = {