Skip to content

feat(desktop): preload pinned+visible transcripts into the render cache - #292

Merged
Kyzcreig merged 1 commit into
mainfrom
feat/transcript-preload
Jul 11, 2026
Merged

Kyzcreig merged 1 commit into
mainfrom
feat/transcript-preload

Conversation

@Kyzcreig

Copy link
Copy Markdown
Collaborator

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).

…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).
@Kyzcreig
Kyzcreig force-pushed the feat/transcript-preload branch from 7625d94 to b1d8fcb Compare July 11, 2026 05:20
@Kyzcreig
Kyzcreig merged commit 5dbe073 into main Jul 11, 2026
24 checks passed
@Kyzcreig
Kyzcreig deleted the feat/transcript-preload branch July 11, 2026 05:24
@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a background transcript preloader that fires 5 seconds after boot completes, sequentially fetching up to 12 pinned+visible sessions' transcripts into the render cache at a 750ms-paced rate. The implementation is well-isolated — dependency-injected for testability, guarded by a module-level once-per-launch flag, and fully fail-open.

  • transcript-preload.ts: New module with selectPreloadSessions (pinned-first ordering, archives excluded, capped at MAX_PRELOAD=12) and preloadTranscripts (sequential fetcher with shouldStop abort, freshCached skip, and fail-open error handling).
  • use-gateway-boot.ts: Wires the preloader after completeDesktopBoot() with a 5 s setTimeout, capturing preloadUrl at boot time and $sessions at fire time; the shouldStop lambda reads the live cancelled and gatewayOpen() closures for clean abort on unmount or gateway loss.
  • transcript-preload.test.ts: Eight unit tests covering ordering, archival exclusion, sequential fetching, freshness skipping, per-session error tolerance, abort-on-shouldStop, null-URL early exit, and profile passthrough.

Confidence Score: 4/5

Safe to merge; the preloader is strictly additive, fail-open, and never blocks the boot path.

The two findings are both non-blocking: an unnecessary 750 ms sleep at the tail of the preload loop (cosmetic, since the caller ignores the resolved value), and a minor timing asymmetry where the gateway URL is snapshotted at boot while the session list is read 5 seconds later at fire time. Neither affects correctness under normal operation. The core logic — sequential pacing, shouldStop abort, once-per-launch guard, and fail-open error handling — is implemented correctly and well covered by the eight unit tests.

No files require special attention; both findings touch transcript-preload.ts and the integration block in use-gateway-boot.ts.

Important Files Changed

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)
Loading
%%{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)
Loading

Reviews (1): Last reviewed commit: "feat(desktop): preload pinned+visible se..." | Re-trigger Greptile

Comment on lines +77 to +89
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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

Comment on lines +539 to +546
const preloadUrl = hydration.gatewayUrl ?? conn?.baseUrl ?? null
setTimeout(() => {
void preloadTranscripts({
gatewayUrl: preloadUrl,
sessions: $sessions.get(),
shouldStop: () => cancelled || !gatewayOpen()
}).catch(() => undefined)
}, 5_000)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant