Repository navigation
feat(desktop): preload pinned+visible transcripts into the render cache - #292
Conversation
…nder cache After boot fully settles (live reconcile done, gateway open), background-fetch the transcripts of pinned + visible sidebar sessions into the render cache — switching to any session you can see paints instantly, and the next cold launch has them warm. Ace's ask: 'why not preload transcripts that are pinned and/or visible in the session list pane?' Deliberately gentle: starts 5s after boot completes, strictly sequential with a 750ms gap between fetches (the perf work exists to UNLOAD the gateway at boot, not re-hammer it), capped at 12 sessions/launch (pinned first), one pass per launch, stops if the gateway drops. Disk bounded by the render cache's existing 200-file LRU cap; deletes still cull via the existing wire. Tests: 8 unit (ordering/cap/archived-skip, sequential fetch+push, fresh-cache skip, fail-open on fetch error, shouldStop, profile passthrough). Full UI suite 1252 pass; the suite's non-zero exit reproduces on clean base (pre-existing streaming.test.tsx teardown race, stash A/B verified).
7625d94 to
b1d8fcb
Compare
|
| Filename | Overview |
|---|---|
| apps/desktop/src/app/transcript-preload.ts | New preload module — correct logic overall; sleep runs unconditionally after every session including the last, adding an unnecessary 750ms tail; fetchMessages signature and session.profile access are correct per the SessionInfo type. |
| apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts | Integration point is clean; preloadUrl is captured at boot completion (before the 5 s delay) while sessions is captured at fire time, creating a minor timing asymmetry if the gateway URL changes during that window. |
| apps/desktop/src/app/transcript-preload.test.ts | 8 tests cover the main code paths well; { profile: 'daedalus' } as never cast is unnecessary since SessionInfo.profile is profile?: string, but this is cosmetic. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Boot as useGatewayBoot
participant Timer as setTimeout (5 s)
participant Pre as preloadTranscripts
participant GW as Gateway API
participant RC as Render Cache
Boot->>Boot: completeDesktopBoot() / capture preloadUrl
Boot->>Timer: schedule preload (5 000 ms)
Note over Boot: transcriptPreloadStartedRef = true
Timer->>Pre: "preloadTranscripts({ gatewayUrl, sessions, shouldStop })"
loop for each session in selectPreloadSessions (pinned first, max 12)
Pre->>Pre: shouldStop()? break if cancelled / gateway closed
Pre->>Pre: freshCached.has(id)? skip if already warm
Pre->>GW: getSessionMessages(id, profile)
GW-->>Pre: "{ messages }"
alt "messages.length > 0"
Pre->>RC: pushTranscriptToRenderCache(url, id, rows)
end
Pre->>Pre: sleep(750 ms)
end
Pre-->>Timer: cached count (void / ignored)
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Boot as useGatewayBoot
participant Timer as setTimeout (5 s)
participant Pre as preloadTranscripts
participant GW as Gateway API
participant RC as Render Cache
Boot->>Boot: completeDesktopBoot() / capture preloadUrl
Boot->>Timer: schedule preload (5 000 ms)
Note over Boot: transcriptPreloadStartedRef = true
Timer->>Pre: "preloadTranscripts({ gatewayUrl, sessions, shouldStop })"
loop for each session in selectPreloadSessions (pinned first, max 12)
Pre->>Pre: shouldStop()? break if cancelled / gateway closed
Pre->>Pre: freshCached.has(id)? skip if already warm
Pre->>GW: getSessionMessages(id, profile)
GW-->>Pre: "{ messages }"
alt "messages.length > 0"
Pre->>RC: pushTranscriptToRenderCache(url, id, rows)
end
Pre->>Pre: sleep(750 ms)
end
Pre-->>Timer: cached count (void / ignored)
Reviews (1): Last reviewed commit: "feat(desktop): preload pinned+visible se..." | Re-trigger Greptile
| try { | ||
| const result = await fetchMessages(session.id, session.profile) | ||
| const rows = Array.isArray(result?.messages) ? result.messages : [] | ||
| if (rows.length > 0) { | ||
| push(gatewayUrl, session.id, rows) | ||
| cached += 1 | ||
| } | ||
| } catch { | ||
| // fail-open: skip this session, keep going | ||
| } | ||
| await sleep(PRELOAD_GAP_MS) | ||
| } | ||
| return cached |
There was a problem hiding this comment.
Unnecessary sleep after the last session. The
await sleep(PRELOAD_GAP_MS) sits unconditionally at the bottom of the loop body, so it fires even after the final session is processed. Since the for-of iterator is exhausted immediately after, this adds a gratuitous 750 ms before the function resolves. The gap is meant to pace between fetches, not after the last one.
| try { | |
| const result = await fetchMessages(session.id, session.profile) | |
| const rows = Array.isArray(result?.messages) ? result.messages : [] | |
| if (rows.length > 0) { | |
| push(gatewayUrl, session.id, rows) | |
| cached += 1 | |
| } | |
| } catch { | |
| // fail-open: skip this session, keep going | |
| } | |
| await sleep(PRELOAD_GAP_MS) | |
| } | |
| return cached | |
| try { | |
| const result = await fetchMessages(session.id, session.profile) | |
| const rows = Array.isArray(result?.messages) ? result.messages : [] | |
| if (rows.length > 0) { | |
| push(gatewayUrl, session.id, rows) | |
| cached += 1 | |
| } | |
| } catch { | |
| // fail-open: skip this session, keep going | |
| } | |
| const remaining = selectPreloadSessions(sessions) | |
| if (remaining.indexOf(session) < remaining.length - 1) { | |
| await sleep(PRELOAD_GAP_MS) | |
| } | |
| } | |
| return cached |
| const preloadUrl = hydration.gatewayUrl ?? conn?.baseUrl ?? null | ||
| setTimeout(() => { | ||
| void preloadTranscripts({ | ||
| gatewayUrl: preloadUrl, | ||
| sessions: $sessions.get(), | ||
| shouldStop: () => cancelled || !gatewayOpen() | ||
| }).catch(() => undefined) | ||
| }, 5_000) |
There was a problem hiding this comment.
preloadUrl captured at boot, sessions captured at fire time
preloadUrl is frozen at the moment completeDesktopBoot() returns, while $sessions.get() is evaluated 5 seconds later when the timeout fires. These two snapshots belong to different points in time. If the gateway URL were to change in that window (e.g. a fast gateway-switch on a slow machine), the preload would write transcripts into the wrong cache bucket while the session list already reflects the new gateway. Capturing both at the same moment — either both eagerly or both lazily — would make them consistent.
Follow-up to #287. After boot settles, gently warm the render cache with pinned + visible sessions' transcripts → instant switch to anything you can see in the sidebar, and warm next-launch paints.
Gentle by design: starts 5s post-boot, sequential with 750ms gaps, 12-session cap (pinned first), once per launch, aborts if the gateway drops. Disk bounded by the existing 200-file LRU cap.
8 unit tests; full UI suite 1252 pass (non-zero exit is a pre-existing base teardown race, A/B verified).