fix(desktop): keep last-known remote roster rows when the union probe fails
When host.agents() rejects (e.g. a registered WSL backend's localhost forwarding went down), the roster query's catch path returned a local-only snapshot, so every previously painted remote Bot row vanished from Bot Mode on the next 5s refresh even though the connection stayed registered (#98844). Merge the last-known $lastRoster rows through the same mergeMultiSourceRoster path used for connect-on-demand sources, marking carried remote rows sourceReachable: false so they paint as offline instead of disappearing. sources is intentionally omitted from the fallback payload so the pane keeps its remembered source snapshot. Empty-previous behavior is unchanged (local-only list stands).
This commit is contained in:
@@ -574,6 +574,42 @@ describe('connect-on-demand sources', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('an aggregate roster failure keeps the last-known remote rows', () => {
|
||||
// Same shape the pane paints: one rich local row + one remote row already
|
||||
// carried into $lastRoster by a previous successful refresh.
|
||||
const previouslyPainted = (connectionId: string) =>
|
||||
[
|
||||
{ last_session: { id: 'this-chat', last_active: 1 }, name: 'default' },
|
||||
{
|
||||
connectionId,
|
||||
connectionKind: 'ssh',
|
||||
connectionLabel: 'Spark',
|
||||
handle: 'bob',
|
||||
name: 'bob',
|
||||
remoteSource: true,
|
||||
sourceScoped: true
|
||||
}
|
||||
] as RosterRow[]
|
||||
|
||||
it('retains previously painted remote rows as unreachable when host.agents() rejects', async () => {
|
||||
// mergedRoster(local, null) rejects host.agents(): the union probe fails
|
||||
// (e.g. a registered WSL backend's localhost forwarding went down) while
|
||||
// the active source's profiles.list still answers (#98844).
|
||||
$lastRoster.set(previouslyPainted('spark'))
|
||||
|
||||
const rows = await mergedRoster(
|
||||
{ profiles: [{ last_session: { id: 'this-chat', last_active: 1 }, name: 'default' }] },
|
||||
null
|
||||
)
|
||||
|
||||
expect(rows.find(row => row.name === 'bob' && row.connectionId === 'spark')).toMatchObject({
|
||||
remoteSource: true,
|
||||
sourceReachable: false
|
||||
})
|
||||
expect(rows.filter(row => row.name === 'default')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('a stalled profiles.list cannot pin the spinner forever', () => {
|
||||
it('gives up after bounded retries and surfaces the error', async () => {
|
||||
// `retry: true` keeps React Query in isLoading until the first success, so
|
||||
|
||||
@@ -693,7 +693,21 @@ export function useRoster() {
|
||||
fetchedAt: issuedAt
|
||||
}
|
||||
} catch {
|
||||
/* older build or roster failure — single-source list stands */
|
||||
/* Aggregate-roster failure, not a registry change: keep the
|
||||
* previously painted remote rows on the roster as unreachable
|
||||
* instead of repainting Bot Mode local-only (#98844). `sources`
|
||||
* stays absent so the pane falls back to its remembered source
|
||||
* snapshot rather than clearing it. */
|
||||
const previous: RosterRow[] = $lastRoster.get().filter(row => !row?.ghost)
|
||||
const merged = mergeMultiSourceRoster(local, null, activeConnectionId, previous)
|
||||
|
||||
return {
|
||||
...merged,
|
||||
profiles: (merged?.profiles || []).map(row =>
|
||||
row?.remoteSource ? { ...row, sourceReachable: false } : row
|
||||
),
|
||||
fetchedAt: issuedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user