fix(desktop): restored background tabs resolve their session title without a click
A restored session tile has no runtimeId and never mounts its pane until first activation, so the by-id resolution effect inside SessionTilePane never runs. When the row is also outside the recents page and project tree, tileTitle() falls back to "New session" until the user clicks the tab (#94167). Add a one-shot backfill, wired next to watchSessionTiles(): once the gateway is open, look each unrestored, untitled, unlisted tile up via resolveStoredSession(id, tile.ownerRoute). That call already upserts the row into $sessions, which the tab strip watches, so the tab renames itself — nothing new is persisted and workspaceTabTitle stays the Bot Chat marker. Live repro (Electron e2e, target session pushed off the 50-row recents page by 60 newer sessions, restored as a stacked background tab): main showed "New session" after boot with no click; with this fix the tab reads the real title while the pane is still unmounted. Closes #94167 Supersedes #94212 Co-authored-by: 686f6c61 <github@00b.tech>
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { sessionTileResumeFailure } from './session-tile'
|
||||
import { $gatewayState, $sessions, setSessions } from '@/store/session'
|
||||
import { $sessionTiles } from '@/store/session-states'
|
||||
|
||||
import { sessionTileResumeFailure, startUnrestoredTileTitleBackfill } from './session-tile'
|
||||
|
||||
describe('sessionTileResumeFailure', () => {
|
||||
it('keeps a confirmed durable session retryable instead of repeating a stale 404', () => {
|
||||
@@ -17,3 +20,43 @@ describe('sessionTileResumeFailure', () => {
|
||||
expect(sessionTileResumeFailure('session not found', true, false)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('startUnrestoredTileTitleBackfill (#94167)', () => {
|
||||
afterEach(() => {
|
||||
$gatewayState.set('idle')
|
||||
$sessionTiles.set([])
|
||||
setSessions([])
|
||||
})
|
||||
|
||||
it('backfills unlisted unrestored tiles by id via their ownerRoute once the gateway opens', async () => {
|
||||
const ownerRoute = { connectionId: 'conn-a', profile: 'writer' }
|
||||
setSessions([{ id: 'listed', title: 'Already listed' } as never])
|
||||
$sessionTiles.set([
|
||||
{ ownerRoute, storedSessionId: 'old-chat' },
|
||||
{ storedSessionId: 'listed' },
|
||||
{ runtimeId: 'rt-live', storedSessionId: 'live' },
|
||||
{ storedSessionId: 'bot', workspaceTabTitle: 'Bot Chat' }
|
||||
])
|
||||
|
||||
const lookup = vi.fn(async (id: string) => {
|
||||
const row = { id, title: 'Quarterly review' } as never
|
||||
setSessions(prev => [row, ...prev])
|
||||
|
||||
return row
|
||||
})
|
||||
|
||||
const stop = startUnrestoredTileTitleBackfill(lookup as never)
|
||||
expect(lookup).not.toHaveBeenCalled()
|
||||
|
||||
$gatewayState.set('open')
|
||||
await vi.waitFor(() => expect(lookup).toHaveBeenCalledTimes(1))
|
||||
expect(lookup).toHaveBeenCalledWith('old-chat', ownerRoute)
|
||||
expect($sessions.get().find(row => row.id === 'old-chat')?.title).toBe('Quarterly review')
|
||||
|
||||
// One-shot: a later reconnect does not re-probe.
|
||||
$gatewayState.set('idle')
|
||||
$gatewayState.set('open')
|
||||
expect(lookup).toHaveBeenCalledTimes(1)
|
||||
stop()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -478,6 +478,33 @@ export function tileStoredRow(storedSessionId: string): SessionInfo | undefined
|
||||
)
|
||||
}
|
||||
|
||||
/** One-shot by-id title fill for restored tiles that never mount (#94167).
|
||||
* A restored background tab has no runtimeId and does not mount its pane, so
|
||||
* the resolution effect above never runs; when its row is outside the recents
|
||||
* page and project tree, `tileTitle()` reads "New session" until first click.
|
||||
* `resolveStoredSession` upserts the row into `$sessions`, which the tab strip
|
||||
* already watches — nothing is persisted. Runs once the gateway can answer. */
|
||||
export function startUnrestoredTileTitleBackfill(lookup = resolveStoredSession): () => void {
|
||||
const run = () => {
|
||||
if ($gatewayState.get() !== 'open') {
|
||||
return
|
||||
}
|
||||
|
||||
off()
|
||||
|
||||
for (const tile of $sessionTiles.get()) {
|
||||
if (!tile.runtimeId && !tile.workspaceTabTitle && !tileStoredRow(tile.storedSessionId)) {
|
||||
void lookup(tile.storedSessionId, tile.ownerRoute).catch(() => undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const off = $gatewayState.listen(run)
|
||||
run()
|
||||
|
||||
return off
|
||||
}
|
||||
|
||||
/** The tab's REGISTERED name. Deliberately the bare placeholder for a draft
|
||||
* rather than its live composer title (`tabTitle` renders that): re-registering
|
||||
* per keystroke would re-render the strip, and holding the draft's text here
|
||||
|
||||
@@ -84,6 +84,7 @@ import { startSessionDrag } from '../chat/session-drag'
|
||||
import {
|
||||
SessionTileCloseConfirm,
|
||||
stackSessionTilesIntoMain,
|
||||
startUnrestoredTileTitleBackfill,
|
||||
watchSessionTiles,
|
||||
WorkspaceTabMenu
|
||||
} from '../chat/session-tile'
|
||||
@@ -459,6 +460,7 @@ watchContributedPanes()
|
||||
// into the transparent overlay).
|
||||
if (!isBrowserWindow() && !isHudWindow()) {
|
||||
watchSessionTiles()
|
||||
startUnrestoredTileTitleBackfill()
|
||||
watchRouteTiles()
|
||||
watchPreviewTiles()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user