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:
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user