Browse Source

fix(app): make session navigation stable and fast (#33569)

Co-authored-by: Brendan Allan <git@brendonovich.dev>
Luke Parker 2 months ago
parent
commit
a4551a94b4
29 changed files with 1028 additions and 202 deletions
  1. 2 0
      packages/app/e2e/performance/README.md
  2. 87 0
      packages/app/e2e/performance/timeline/first-navigation-benchmark.spec.ts
  3. 34 0
      packages/app/e2e/performance/timeline/first-navigation-metrics.ts
  4. 85 0
      packages/app/e2e/performance/timeline/first-navigation-probe.ts
  5. 114 0
      packages/app/e2e/performance/timeline/home-tab-navigation-benchmark.spec.ts
  6. 123 0
      packages/app/e2e/performance/timeline/navigation-milestones.ts
  7. 18 0
      packages/app/e2e/performance/timeline/session-tab-flash.spec.ts
  8. 40 4
      packages/app/e2e/performance/timeline/session-timeline-stress.fixture.ts
  9. 31 15
      packages/app/e2e/performance/timeline/timeline-test-helpers.ts
  10. 32 0
      packages/app/e2e/performance/unit/first-navigation-metrics.test.ts
  11. 34 0
      packages/app/e2e/performance/unit/navigation-milestones.test.ts
  12. 10 0
      packages/app/e2e/performance/unit/timeline-test-helpers.test.ts
  13. 1 1
      packages/app/e2e/regression/review-line-comment.spec.ts
  14. 12 1
      packages/app/e2e/smoke/session-timeline.spec.ts
  15. 11 4
      packages/app/e2e/utils/mock-server.ts
  16. 68 68
      packages/app/src/app.tsx
  17. 52 12
      packages/app/src/components/titlebar.tsx
  18. 1 1
      packages/app/src/context/directory-sync.ts
  19. 3 0
      packages/app/src/context/global.tsx
  20. 7 1
      packages/app/src/pages/directory-layout.tsx
  21. 46 1
      packages/app/src/pages/home.tsx
  22. 26 4
      packages/app/src/pages/session.tsx
  23. 3 15
      packages/app/src/pages/session/timeline/model.test.ts
  24. 6 22
      packages/app/src/pages/session/timeline/model.ts
  25. 38 0
      packages/app/src/utils/session-placement.test.ts
  26. 41 0
      packages/app/src/utils/session-placement.ts
  27. 78 0
      packages/ui/src/components/markdown-cache.tsx
  28. 18 0
      packages/ui/src/components/markdown-preload.test.ts
  29. 7 53
      packages/ui/src/components/markdown.tsx

+ 2 - 0
packages/app/e2e/performance/README.md

@@ -18,6 +18,8 @@ bun run test:bench
 The suite contains:
 
 - cold and hot session-tab timing
+- home-session click timing split between content and titlebar-tab paint
+- single-session tab close timing through stable home restoration
 - cached session repaint and mutation tracing
 - streaming timeline throughput, RAF-gap, long-task, geometry, and remount diagnostics
 

+ 87 - 0
packages/app/e2e/performance/timeline/first-navigation-benchmark.spec.ts

@@ -0,0 +1,87 @@
+import { expectSessionTitle } from "../../utils/waits"
+import { benchmark, expect } from "../benchmark"
+import { measureFirstNavigation } from "./first-navigation-probe"
+import { fixture } from "./session-timeline-stress.fixture"
+import {
+  installStressSessionTabs,
+  installTimelineSettings,
+  mockStressTimeline,
+  stressDraftHref,
+  stressSessionHref,
+} from "./timeline-test-helpers"
+import { waitForStableTimeline } from "./session-tab-switch-probe"
+
+const contentSelector = '[data-message-id], [data-component="prompt-input"]'
+const draftID = "draft_first_navigation"
+
+benchmark.describe("performance: first navigation paint", () => {
+  benchmark("opens an unvisited session tab without a blank frame", async ({ page, report }) => {
+    await setup(page)
+    const href = stressSessionHref(fixture.targetID)
+    const result = await measureFirstNavigation(page, {
+      href,
+      destinationPath: href,
+      sourceSelector: messageSelector(fixture.expected.sourceMessageIDs.at(-1)!),
+      destinationSelector: messageSelector(fixture.expected.targetMessageIDs.at(-1)!),
+      contentSelector,
+      navigate: async () => {
+        await page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first().click()
+        await expectSessionTitle(page, fixture.expected.targetTitle)
+      },
+    })
+    report(result)
+    expect(result.summary.blankSamples).toBe(0)
+    expect(result.summary.unknownSamples).toBe(0)
+  })
+
+  benchmark("opens the new session page before its lazy module is used", async ({ page, report }) => {
+    await setup(page, draftID)
+    const href = stressDraftHref(draftID)
+    const result = await measureFirstNavigation(page, {
+      href,
+      destinationPath: href,
+      sourceSelector: messageSelector(fixture.expected.sourceMessageIDs.at(-1)!),
+      destinationSelector: '[data-component="prompt-input"]',
+      contentSelector,
+      navigate: async () => {
+        await page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first().click()
+        await expect(page.locator('[data-component="prompt-input"]')).toBeVisible()
+      },
+    })
+    report(result)
+    expect(result.summary.blankSamples).toBe(0)
+    expect(result.summary.unknownSamples).toBe(0)
+  })
+
+  benchmark("opens a child session without a blank frame", async ({ page, report }) => {
+    await setup(page)
+    const href = stressSessionHref(fixture.childID)
+    const result = await measureFirstNavigation(page, {
+      href,
+      destinationPath: href,
+      sourceSelector: messageSelector(fixture.expected.sourceMessageIDs.at(-1)!),
+      destinationSelector: messageSelector(fixture.expected.childMessageIDs.at(-1)!),
+      contentSelector,
+      navigate: async () => {
+        await page.locator(`a[href="${href}"]`, { has: page.locator('[data-component="task-tool-card"]') }).click()
+        await expectSessionTitle(page, fixture.expected.childTitle)
+      },
+    })
+    report(result)
+    expect(result.summary.blankSamples).toBe(0)
+    expect(result.summary.unknownSamples).toBe(0)
+  })
+})
+
+async function setup(page: Parameters<typeof mockStressTimeline>[0], draft?: string) {
+  await mockStressTimeline(page)
+  await installTimelineSettings(page)
+  await installStressSessionTabs(page, draft ? { draftID: draft } : undefined)
+  await page.goto(stressSessionHref(fixture.sourceID))
+  await expectSessionTitle(page, fixture.expected.sourceTitle)
+  await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!)
+}
+
+function messageSelector(id: string) {
+  return `[data-message-id="${id}"]`
+}

+ 34 - 0
packages/app/e2e/performance/timeline/first-navigation-metrics.ts

@@ -0,0 +1,34 @@
+export type FirstNavigationSample = {
+  observedAtMs: number
+  source: boolean
+  destination: boolean
+  content: boolean
+  pathname?: string
+  center?: string
+}
+
+function category(sample: FirstNavigationSample) {
+  if (sample.destination && !sample.source) return "destination"
+  if (sample.source && !sample.destination) return "source"
+  if (!sample.content) return "blank"
+  return "unknown"
+}
+
+export function summarizeFirstNavigation(samples: FirstNavigationSample[]) {
+  const categories = samples.map(category)
+  const stable = categories.findIndex(
+    (value, index) =>
+      value === "destination" &&
+      categories[index + 1] === "destination" &&
+      categories[index + 2] === "destination",
+  )
+  return {
+    samples: samples.length,
+    firstDestinationObservedMs: samples[categories.indexOf("destination")]?.observedAtMs ?? null,
+    stableDestinationObservedMs: stable === -1 ? null : samples[stable + 2]!.observedAtMs,
+    sourceSamples: categories.filter((value) => value === "source").length,
+    blankSamples: categories.filter((value) => value === "blank").length,
+    unknownSamples: categories.filter((value) => value === "unknown").length,
+    destinationSamples: categories.filter((value) => value === "destination").length,
+  }
+}

+ 85 - 0
packages/app/e2e/performance/timeline/first-navigation-probe.ts

@@ -0,0 +1,85 @@
+import type { Page } from "@playwright/test"
+import { summarizeFirstNavigation, type FirstNavigationSample } from "./first-navigation-metrics"
+
+type FirstNavigationProbe = {
+  samples: FirstNavigationSample[]
+  stop: () => void
+}
+
+export async function measureFirstNavigation(
+  page: Page,
+  input: {
+    href: string
+    destinationPath: string
+    sourceSelector: string
+    destinationSelector: string
+    contentSelector: string
+    navigate: () => Promise<void>
+  },
+) {
+  await page.evaluate(
+    ({ href, destinationPath, sourceSelector, destinationSelector, contentSelector }) => {
+      const samples: FirstNavigationSample[] = []
+      let started: number | undefined
+      let running = true
+      const visible = (selector: string) =>
+        [...document.querySelectorAll<HTMLElement>(selector)].some((element) => {
+          const rect = element.getBoundingClientRect()
+          const style = getComputedStyle(element)
+          return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none"
+        })
+      const sample = () => {
+        if (!running || started === undefined) return
+        requestAnimationFrame(() => {
+          setTimeout(() => {
+            if (!running || started === undefined) return
+            samples.push({
+              observedAtMs: performance.now() - started,
+              source: visible(sourceSelector),
+              destination: `${location.pathname}${location.search}` === destinationPath && visible(destinationSelector),
+              content: visible(contentSelector),
+              pathname: `${location.pathname}${location.search}`,
+              center: document.elementFromPoint(innerWidth / 2, innerHeight / 2)?.textContent?.slice(0, 80),
+            })
+            sample()
+          }, 0)
+        })
+      }
+      document.addEventListener(
+        "click",
+        (event) => {
+          const link = event.target instanceof Element ? event.target.closest("a") : undefined
+          if (link?.getAttribute("href") !== href) return
+          started = performance.now()
+          sample()
+        },
+        { capture: true, once: true },
+      )
+      ;(window as Window & { __firstNavigationProbe?: FirstNavigationProbe }).__firstNavigationProbe = {
+        samples,
+        stop: () => {
+          running = false
+        },
+      }
+    },
+    {
+      href: input.href,
+      destinationPath: input.destinationPath,
+      sourceSelector: input.sourceSelector,
+      destinationSelector: input.destinationSelector,
+      contentSelector: input.contentSelector,
+    },
+  )
+  await input.navigate()
+  await page.waitForFunction(() => {
+    const samples = (window as Window & { __firstNavigationProbe?: FirstNavigationProbe }).__firstNavigationProbe?.samples
+    if (!samples) return false
+    return samples.length >= 3 && samples.slice(-3).every((sample) => sample.destination && !sample.source)
+  })
+  const samples = await page.evaluate(() => {
+    const probe = (window as Window & { __firstNavigationProbe?: FirstNavigationProbe }).__firstNavigationProbe!
+    probe.stop()
+    return probe.samples
+  })
+  return { summary: summarizeFirstNavigation(samples), samples }
+}

+ 114 - 0
packages/app/e2e/performance/timeline/home-tab-navigation-benchmark.spec.ts

@@ -0,0 +1,114 @@
+import { benchmark, expect } from "../benchmark"
+import { expectSessionTitle } from "../../utils/waits"
+import { measureNavigationMilestones } from "./navigation-milestones"
+import { fixture } from "./session-timeline-stress.fixture"
+import {
+  installStressSessionTabs,
+  installTimelineSettings,
+  mockStressTimeline,
+  stressSessionHref,
+} from "./timeline-test-helpers"
+import { waitForStableTimeline } from "./session-tab-switch-probe"
+
+const homeRow = '[data-component="home-session-row"]'
+const homeShell = '[data-component="home-session-search"]'
+
+benchmark.describe("performance: home and tab navigation", () => {
+  benchmark("opens a home session and paints its titlebar tab", async ({ page, report }) => {
+    await setup(page, [])
+    await page.goto("/")
+    const row = page.locator(homeRow).filter({ hasText: fixture.expected.targetTitle }).first()
+    await expect(row).toBeVisible()
+    const href = stressSessionHref(fixture.targetID)
+    const result = await measureNavigationMilestones(page, {
+      triggerSelector: homeRow,
+      milestones: {
+        content: { selector: messageSelector(fixture.expected.targetMessageIDs.at(-1)!) },
+        tab: { selector: `[data-slot="titlebar-tabs"] a[href="${href}"]` },
+      },
+      navigate: async () => {
+        await row.click()
+        await expectSessionTitle(page, fixture.expected.targetTitle)
+      },
+    })
+    report(result)
+    await expect(page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`)).toContainText(
+      fixture.expected.targetTitle,
+    )
+  })
+
+  benchmark("stages the review body after cold session content", async ({ page, report }) => {
+    await setup(page, [])
+    await page.goto("/")
+    const row = page.locator(homeRow).filter({ hasText: fixture.expected.targetTitle }).first()
+    await expect(row).toBeVisible()
+    const result = await page.evaluate(
+      ({ rowSelector, title, contentSelector }) =>
+        new Promise<{ contentBeforeReview: boolean; samples: number }>((resolve) => {
+          let samples = 0
+          const sample = () => {
+            samples++
+            const content = !!document.querySelector(contentSelector)
+            const review = !!document.querySelector('[data-component="session-review"]')
+            if (content && !review) {
+              resolve({ contentBeforeReview: true, samples })
+              return
+            }
+            if (content && review) {
+              resolve({ contentBeforeReview: false, samples })
+              return
+            }
+            requestAnimationFrame(sample)
+          }
+          const target = [...document.querySelectorAll<HTMLElement>(rowSelector)].find((item) =>
+            item.textContent?.includes(title),
+          )
+          if (!target) throw new Error(`Home session row not found: ${title}`)
+          target.click()
+          requestAnimationFrame(sample)
+        }),
+      {
+        rowSelector: homeRow,
+        title: fixture.expected.targetTitle,
+        contentSelector: messageSelector(fixture.expected.targetMessageIDs.at(-1)!),
+      },
+    )
+    report(result)
+    expect(result.contentBeforeReview).toBe(true)
+    await expect(page.locator('[data-component="session-review"]')).toBeVisible()
+  })
+
+  benchmark("closes the only session tab and paints home", async ({ page, report }) => {
+    await setup(page, [fixture.sourceID])
+    const href = stressSessionHref(fixture.sourceID)
+    await page.goto(href)
+    await expectSessionTitle(page, fixture.expected.sourceTitle)
+    await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!)
+    const tab = page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first()
+    const close = tab.locator("..").locator('[data-component="icon-button-v2"]')
+    await expect(close).toBeVisible()
+    const result = await measureNavigationMilestones(page, {
+      triggerSelector: '[data-slot="titlebar-tabs"] [data-component="icon-button-v2"]',
+      milestones: {
+        home: { selector: homeShell },
+        row: { selector: homeRow },
+        tabRemoved: { selector: `[data-slot="titlebar-tabs"] a[href="${href}"]`, visible: false },
+      },
+      navigate: async () => {
+        await close.click()
+        await expect(page).toHaveURL("/")
+      },
+    })
+    report(result)
+  })
+})
+
+async function setup(page: Parameters<typeof mockStressTimeline>[0], sessionIDs: string[]) {
+  await mockStressTimeline(page)
+  await installTimelineSettings(page)
+  await installStressSessionTabs(page, { sessionIDs })
+}
+
+function messageSelector(id: string) {
+  return `[data-message-id="${id}"]`
+}

+ 123 - 0
packages/app/e2e/performance/timeline/navigation-milestones.ts

@@ -0,0 +1,123 @@
+import type { Page } from "@playwright/test"
+
+export type NavigationMilestoneSample = {
+  observedAtMs: number
+  milestones: Record<string, boolean>
+}
+
+export function summarizeNavigationMilestones(samples: NavigationMilestoneSample[]) {
+  const names = Object.keys(samples[0]?.milestones ?? {})
+  const summarize = (matches: (sample: NavigationMilestoneSample) => boolean) => {
+    const first = samples.find(matches)
+    const stable = samples.findIndex(
+      (sample, index) =>
+        index + 2 < samples.length && matches(sample) && matches(samples[index + 1]!) && matches(samples[index + 2]!),
+    )
+    return {
+      firstObservedMs: first?.observedAtMs ?? null,
+      stableObservedMs: stable === -1 ? null : samples[stable + 2]!.observedAtMs,
+    }
+  }
+  return {
+    samples: samples.length,
+    milestones: Object.fromEntries(names.map((name) => [name, summarize((sample) => sample.milestones[name] === true)])),
+    all: summarize((sample) => names.every((name) => sample.milestones[name] === true)),
+  }
+}
+
+type NavigationMilestoneProbe = {
+  samples: NavigationMilestoneSample[]
+  stop: () => void
+}
+
+export async function measureNavigationMilestones(
+  page: Page,
+  input: {
+    triggerSelector: string
+    milestones: Record<string, { selector: string; visible?: boolean }>
+    navigate: () => Promise<void>
+  },
+) {
+  await page.evaluate(({ triggerSelector, milestones }) => {
+    const samples: NavigationMilestoneSample[] = []
+    const streaks = new Map<string, number>()
+    const marked = new Set<string>()
+    let started: number | undefined
+    let running = true
+    const visible = (selector: string) =>
+      [...document.querySelectorAll<HTMLElement>(selector)].some((element) => {
+        const rect = element.getBoundingClientRect()
+        const style = getComputedStyle(element)
+        return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none"
+      })
+    const sample = () => {
+      if (!running || started === undefined) return
+      requestAnimationFrame(() => {
+        setTimeout(() => {
+          if (!running || started === undefined) return
+          const current = Object.fromEntries(
+            Object.entries(milestones).map(([name, milestone]) => [
+              name,
+              milestone.visible === false ? !document.querySelector(milestone.selector) : visible(milestone.selector),
+            ]),
+          )
+          samples.push({
+            observedAtMs: performance.now() - started,
+            milestones: current,
+          })
+          Object.entries(current).forEach(([name, value]) => {
+            if (!value) {
+              streaks.set(name, 0)
+              return
+            }
+            if (!marked.has(`${name}.first`)) {
+              performance.mark(`opencode.navigation.${name}.first`)
+              marked.add(`${name}.first`)
+            }
+            const streak = (streaks.get(name) ?? 0) + 1
+            streaks.set(name, streak)
+            if (streak === 3) performance.mark(`opencode.navigation.${name}.stable`)
+          })
+          const all = Object.values(current).every(Boolean)
+          const allStreak = all ? (streaks.get("all") ?? 0) + 1 : 0
+          streaks.set("all", allStreak)
+          if (all && !marked.has("all.first")) {
+            performance.mark("opencode.navigation.all.first")
+            marked.add("all.first")
+          }
+          if (allStreak === 3) performance.mark("opencode.navigation.all.stable")
+          sample()
+        }, 0)
+      })
+    }
+    document.addEventListener(
+      "click",
+      (event) => {
+        if (!(event.target instanceof Element) || !event.target.closest(triggerSelector)) return
+        started = performance.now()
+        performance.mark("opencode.navigation.click")
+        sample()
+      },
+      { capture: true, once: true },
+    )
+    ;(window as Window & { __navigationMilestones?: NavigationMilestoneProbe }).__navigationMilestones = {
+      samples,
+      stop: () => {
+        running = false
+      },
+    }
+  }, { triggerSelector: input.triggerSelector, milestones: input.milestones })
+  await input.navigate()
+  await page.waitForFunction(() => {
+    const samples = (window as Window & { __navigationMilestones?: NavigationMilestoneProbe }).__navigationMilestones
+      ?.samples
+    if (!samples || samples.length < 3) return false
+    return samples.slice(-3).every((sample) => Object.values(sample.milestones).every(Boolean))
+  })
+  const samples = await page.evaluate(() => {
+    const probe = (window as Window & { __navigationMilestones?: NavigationMilestoneProbe }).__navigationMilestones!
+    probe.stop()
+    return probe.samples
+  })
+  return { summary: summarizeNavigationMilestones(samples), samples }
+}

+ 18 - 0
packages/app/e2e/performance/timeline/session-tab-flash.spec.ts

@@ -47,3 +47,21 @@ benchmark("samples cached session repaint after the click", async ({ page, repor
   report(compressCachedRepaintTrace(result))
   expect(result.samples.length).toBeGreaterThan(0)
 })
+
+benchmark("prefetches every open session tab", async ({ page, report }) => {
+  const prefetched = new Set<string>()
+  await mockStressTimeline(page, {
+    onMessages: (input) => {
+      if (!input.before && input.phase === "start") prefetched.add(input.sessionID)
+    },
+  })
+  await installStressSessionTabs(page, {
+    sessionIDs: [fixture.sourceID, fixture.targetID, fixture.childID],
+  })
+  await installTimelineSettings(page)
+  await page.goto(stressSessionHref(fixture.sourceID))
+  await expectSessionTitle(page, fixture.expected.sourceTitle)
+
+  await expect.poll(() => prefetched.has(fixture.childID)).toBe(true)
+  report({ prefetched: [...prefetched] })
+})

+ 40 - 4
packages/app/e2e/performance/timeline/session-timeline-stress.fixture.ts

@@ -23,6 +23,7 @@ const words = [
 
 const sourceID = "ses_smoke_source"
 const targetID = "ses_smoke_target"
+const childID = "ses_smoke_child"
 const directory = "C:/OpenCode/SmokeProject"
 const projectID = "proj_smoke_timeline"
 const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
@@ -126,9 +127,11 @@ function toolPart(
   tool: string,
   input: Record<string, unknown>,
   outputLength = 160,
+  metadataOverride?: Record<string, unknown>,
 ): MessagePart {
   const metadata =
-    tool === "apply_patch"
+    metadataOverride ??
+    (tool === "apply_patch"
       ? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] }
       : tool === "edit" || tool === "write"
         ? {
@@ -138,7 +141,7 @@ function toolPart(
           }
         : tool === "question"
           ? { answers: [["Proceed"], ["Keep sample output"]] }
-          : {}
+          : {})
   return {
     id: id(`prt_tool_${tool}_${partIndex}`, index),
     type: "tool",
@@ -244,7 +247,25 @@ function turn(index: number): Message[] {
 const targetMessages = Array.from({ length: 72 }, (_, index) => turn(index)).flat()
 const sourceMessages = Array.from({ length: 12 }, (_, index) => [
   userMessage(sourceID, index + 1000, 120),
-  assistantMessage(sourceID, index + 1000, id("msg_user", index + 1000), [textPart(index + 1000, 0, 240)]),
+  assistantMessage(sourceID, index + 1000, id("msg_user", index + 1000), [
+    textPart(index + 1000, 0, 240),
+    ...(index === 11
+      ? [
+          toolPart(
+            index + 1000,
+            1,
+            "task",
+            { description: "Inspect child navigation", subagent_type: "explore" },
+            160,
+            { sessionId: childID },
+          ),
+        ]
+      : []),
+  ]),
+]).flat()
+const childMessages = Array.from({ length: 4 }, (_, index) => [
+  userMessage(childID, index + 2000, 120),
+  assistantMessage(childID, index + 2000, id("msg_user", index + 2000), [textPart(index + 2000, 0, 240)]),
 ]).flat()
 
 function renderable(part: MessagePart) {
@@ -298,19 +319,34 @@ export const fixture = {
       version: "dev",
       time: { created: 1700000001000, updated: 1700000001000 },
     },
+    {
+      id: childID,
+      parentID: sourceID,
+      slug: "child",
+      projectID,
+      directory,
+      title: "Inspect child navigation",
+      version: "dev",
+      time: { created: 1700000002000, updated: 1700000002000 },
+    },
   ],
   sourceID,
   targetID,
-  messages: { [sourceID]: sourceMessages, [targetID]: targetMessages },
+  childID,
+  messages: { [sourceID]: sourceMessages, [targetID]: targetMessages, [childID]: childMessages },
   expected: {
     sourceTitle: "Uncommitted changes inquiry",
     targetTitle: "Example Game: sample jump movement & sample physics analysis",
+    childTitle: "Inspect child navigation",
     sourceMessageIDs: sourceMessages
       .filter((message) => message.info.role === "user")
       .map((message) => message.info.id),
     targetMessageIDs: targetMessages
       .filter((message) => message.info.role === "user")
       .map((message) => message.info.id),
+    childMessageIDs: childMessages
+      .filter((message) => message.info.role === "user")
+      .map((message) => message.info.id),
     targetPartIDs: targetMessages.flatMap((message) =>
       orderedParts(message)
         .filter(renderable)

+ 31 - 15
packages/app/e2e/performance/timeline/timeline-test-helpers.ts

@@ -1,7 +1,7 @@
 import type { Page } from "@playwright/test"
 import { base64Encode } from "@opencode-ai/core/util/encode"
 import { mockOpenCodeServer } from "../../utils/mock-server"
-import { fixture } from "./session-timeline-stress.fixture"
+import { fixture, pageMessages } from "./session-timeline-stress.fixture"
 
 export async function installTimelineSettings(page: Page) {
   await page.addInitScript(() => {
@@ -9,6 +9,7 @@ export async function installTimelineSettings(page: Page) {
       "settings.v3",
       JSON.stringify({
         general: {
+          newLayoutDesigns: true,
           editToolPartsExpanded: true,
           shellToolPartsExpanded: true,
           showReasoningSummaries: true,
@@ -19,20 +20,24 @@ export async function installTimelineSettings(page: Page) {
   })
 }
 
-export function mockStressTimeline(page: Page) {
+export function mockStressTimeline(
+  page: Page,
+  input?: { onMessages?: (input: { sessionID: string; before?: string; phase: "start" | "end" }) => void },
+) {
   return mockOpenCodeServer(page, {
     sessions: fixture.sessions,
     provider: fixture.provider,
     directory: fixture.directory,
     project: fixture.project,
-    pageMessages: (sessionID) => ({ items: fixture.messages[sessionID as keyof typeof fixture.messages] ?? [] }),
+    pageMessages,
+    onMessages: input?.onMessages,
   })
 }
 
-export async function installStressSessionTabs(page: Page) {
-  const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
+export async function installStressSessionTabs(page: Page, input?: { draftID?: string; sessionIDs?: string[] }) {
+  const server = stressServer()
   await page.addInitScript(
-    ({ directory, sourceID, targetID, dirBase64, server }) => {
+    ({ directory, sessionIDs, dirBase64, server, draftID }) => {
       localStorage.setItem(
         "opencode.global.dat:server",
         JSON.stringify({
@@ -43,25 +48,36 @@ export async function installStressSessionTabs(page: Page) {
       localStorage.setItem(
         "opencode.global.dat:tabs",
         JSON.stringify(
-          [sourceID, targetID].map((sessionId) => ({
-            type: "session",
-            server,
-            dirBase64,
-            sessionId,
-          })),
+          [
+            ...sessionIDs.map((sessionId) => ({
+              type: "session",
+              server,
+              dirBase64,
+              sessionId,
+            })),
+            ...(draftID ? [{ type: "draft", draftID, server, directory }] : []),
+          ],
         ),
       )
     },
     {
       directory: fixture.directory,
-      sourceID: fixture.sourceID,
-      targetID: fixture.targetID,
+      sessionIDs: input?.sessionIDs ?? [fixture.sourceID, fixture.targetID],
       dirBase64: base64Encode(fixture.directory),
       server,
+      draftID: input?.draftID,
     },
   )
 }
 
 export function stressSessionHref(sessionID: string) {
-  return `/${base64Encode(fixture.directory)}/session/${sessionID}`
+  return `/server/${base64Encode(stressServer())}/session/${sessionID}`
+}
+
+export function stressDraftHref(draftID: string) {
+  return `/new-session?draftId=${encodeURIComponent(draftID)}`
+}
+
+function stressServer() {
+  return `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
 }

+ 32 - 0
packages/app/e2e/performance/unit/first-navigation-metrics.test.ts

@@ -0,0 +1,32 @@
+import { expect, test } from "bun:test"
+import { summarizeFirstNavigation } from "../timeline/first-navigation-metrics"
+
+test("reports blank frames before first destination and stable paint", () => {
+  expect(
+    summarizeFirstNavigation([
+      { observedAtMs: 16, source: true, destination: false, content: true },
+      { observedAtMs: 32, source: false, destination: false, content: false },
+      { observedAtMs: 48, source: false, destination: true, content: true },
+      { observedAtMs: 64, source: false, destination: true, content: true },
+      { observedAtMs: 80, source: false, destination: true, content: true },
+    ]),
+  ).toEqual({
+    samples: 5,
+    firstDestinationObservedMs: 48,
+    stableDestinationObservedMs: 80,
+    sourceSamples: 1,
+    blankSamples: 1,
+    unknownSamples: 0,
+    destinationSamples: 3,
+  })
+})
+
+test("does not report stability for interrupted destination frames", () => {
+  expect(
+    summarizeFirstNavigation([
+      { observedAtMs: 16, source: false, destination: true, content: true },
+      { observedAtMs: 32, source: false, destination: false, content: true },
+      { observedAtMs: 48, source: false, destination: true, content: true },
+    ]).stableDestinationObservedMs,
+  ).toBeNull()
+})

+ 34 - 0
packages/app/e2e/performance/unit/navigation-milestones.test.ts

@@ -0,0 +1,34 @@
+import { expect, test } from "bun:test"
+import { summarizeNavigationMilestones } from "../timeline/navigation-milestones"
+
+test("reports first and stable paint for each navigation milestone", () => {
+  expect(
+    summarizeNavigationMilestones([
+      { observedAtMs: 16, milestones: { content: false, tab: false } },
+      { observedAtMs: 32, milestones: { content: true, tab: false } },
+      { observedAtMs: 48, milestones: { content: true, tab: true } },
+      { observedAtMs: 64, milestones: { content: true, tab: true } },
+      { observedAtMs: 80, milestones: { content: true, tab: true } },
+    ]),
+  ).toEqual({
+    samples: 5,
+    milestones: {
+      content: { firstObservedMs: 32, stableObservedMs: 64 },
+      tab: { firstObservedMs: 48, stableObservedMs: 80 },
+    },
+    all: { firstObservedMs: 48, stableObservedMs: 80 },
+  })
+})
+
+test("reports missing stability when a milestone appears in the final samples", () => {
+  expect(
+    summarizeNavigationMilestones([
+      { observedAtMs: 16, milestones: { content: false } },
+      { observedAtMs: 32, milestones: { content: true } },
+    ]),
+  ).toEqual({
+    samples: 2,
+    milestones: { content: { firstObservedMs: 32, stableObservedMs: null } },
+    all: { firstObservedMs: 32, stableObservedMs: null },
+  })
+})

+ 10 - 0
packages/app/e2e/performance/unit/timeline-test-helpers.test.ts

@@ -0,0 +1,10 @@
+import { expect, test } from "bun:test"
+import { base64Encode } from "@opencode-ai/core/util/encode"
+import { fixture } from "../timeline/session-timeline-stress.fixture"
+import { stressSessionHref } from "../timeline/timeline-test-helpers"
+
+test("builds stress session links for the benchmark server", () => {
+  expect(stressSessionHref(fixture.sourceID)).toBe(
+    `/server/${base64Encode("http://127.0.0.1:4096")}/session/${fixture.sourceID}`,
+  )
+})

+ 1 - 1
packages/app/e2e/regression/review-line-comment.spec.ts

@@ -57,8 +57,8 @@ test("shows a comment button when a line number is hovered", async ({ page }) =>
     await page.mouse.move(0, 0)
     await lineNumber.hover()
     await expect(comment).toBeVisible({ timeout: 500 })
+    await comment.click({ timeout: 500 })
   }).toPass()
-  await comment.click()
   await expect(review.getByRole("textbox")).toBeVisible()
 })
 

+ 12 - 1
packages/app/e2e/smoke/session-timeline.spec.ts

@@ -58,7 +58,18 @@ test.describe("smoke: session timeline", () => {
       await page.mouse.wheel(0, -120)
       await page.waitForTimeout(20)
     }
-    const keys = ["prt_user_text_smoke_0032", "prt_text_2_smoke_0032", "prt_tool_apply_patch_8_smoke_0032"]
+    const keys = await scroller.evaluate((element) => {
+      const view = element.getBoundingClientRect()
+      return [...element.querySelectorAll<HTMLElement>("[data-timeline-part-id]")]
+        .filter((row) => {
+          const rect = row.getBoundingClientRect()
+          return rect.bottom > view.top && rect.top < view.bottom
+        })
+        .map((row) => row.dataset.timelinePartId)
+        .filter((id): id is string => !!id)
+        .slice(0, 3)
+    })
+    expect(keys.length).toBeGreaterThan(0)
     const positions = () =>
       scroller.evaluate((element, keys) => {
         const top = element.getBoundingClientRect().top

+ 11 - 4
packages/app/e2e/utils/mock-server.ts

@@ -26,6 +26,8 @@ export interface MockServerConfig {
 }
 
 export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
+  const cursors = new Map<string, string>()
+  let nextCursor = 0
   const staticRoutes: Record<string, unknown> = {
     "/provider": config.provider,
     "/path": {
@@ -68,13 +70,18 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
 
     const messagesMatch = path.match(/^\/session\/([^/]+)\/message$/)
     if (messagesMatch) {
-      const before = url.searchParams.get("before") ?? undefined
+      const token = url.searchParams.get("before") ?? undefined
+      const before = token ? cursors.get(token) : undefined
+      if (token && !before) return json(route, { error: "Invalid cursor" }, undefined, 400)
       config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "start" })
       if (config.messageDelay) await new Promise((resolve) => setTimeout(resolve, config.messageDelay))
       const limit = Number(url.searchParams.get("limit") ?? 80)
       const pageData = config.pageMessages(messagesMatch[1], limit, before)
       config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "end" })
-      return json(route, pageData.items, pageData.cursor ? { "x-next-cursor": pageData.cursor } : undefined)
+      if (!pageData.cursor) return json(route, pageData.items)
+      const cursor = `cursor_${++nextCursor}`
+      cursors.set(cursor, pageData.cursor)
+      return json(route, pageData.items, { "x-next-cursor": cursor })
     }
 
     if (url.port === targetPort && targetPort !== appPort) return json(route, {})
@@ -82,9 +89,9 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
   })
 }
 
-function json(route: Route, body: unknown, headers?: Record<string, string>) {
+function json(route: Route, body: unknown, headers?: Record<string, string>, status = 200) {
   return route.fulfill({
-    status: 200,
+    status,
     contentType: "application/json",
     headers: {
       "access-control-allow-origin": "*",

+ 68 - 68
packages/app/src/app.tsx

@@ -10,7 +10,7 @@ import { Splash } from "@opencode-ai/ui/logo"
 import { ThemeProvider } from "@opencode-ai/ui/theme/context"
 import { MetaProvider } from "@solidjs/meta"
 import { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router"
-import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/solid-query"
+import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
 import { Effect } from "effect"
 import {
   type Component,
@@ -32,7 +32,7 @@ import { CommentsProvider } from "@/context/comments"
 import { FileProvider } from "@/context/file"
 import { ServerSDKProvider, useServerSDK } from "@/context/server-sdk"
 import { ServerSyncProvider } from "@/context/server-sync"
-import { GlobalProvider } from "@/context/global"
+import { GlobalProvider, useGlobal } from "@/context/global"
 import { HighlightsProvider } from "@/context/highlights"
 import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
 import { LayoutProvider } from "@/context/layout"
@@ -53,87 +53,89 @@ import { ErrorPage } from "./pages/error"
 import { useCheckServerHealth } from "./utils/server-health"
 import { legacySessionHref, requireServerKey, rootSession, sessionHref } from "./utils/session-route"
 
-const LegacyHome = lazy(() => import("@/pages/home").then((module) => ({ default: module.LegacyHome })))
-const NewHome = lazy(() => import("@/pages/home").then((module) => ({ default: module.NewHome })))
-const Session = lazy(() => import("@/pages/session"))
+import Session from "@/pages/session"
+import { NewHome, LegacyHome } from "@/pages/home"
+
 const NewSession = lazy(() => import("@/pages/new-session"))
 
-const SessionRoute = Object.assign(
-  () => {
-    const settings = useSettings()
-    const params = useParams()
-    const [search] = useSearchParams<{ draftId?: string; prompt?: string }>()
-    const sdk = useSDK()
-    const server = useServer()
-    const tabs = useTabs()
-
-    if (params.id && settings.general.newLayoutDesigns()) {
-      return <Navigate href={sessionHref(server.key, params.id)} />
-    }
+const SessionRoute = () => {
+  const settings = useSettings()
+  const params = useParams()
+  const [search] = useSearchParams<{ draftId?: string; prompt?: string }>()
+  const sdk = useSDK()
+  const server = useServer()
+  const tabs = useTabs()
 
-    // When the new layout is enabled, the legacy new-session route (/:dir/session with no id)
-    // is replaced by a draft at /new-session?draftId=…
-    createEffect(() => {
-      if (!settings.general.newLayoutDesigns()) return
-      if (params.id || search.draftId) return
-      if (!tabs.ready() || !sdk().directory) return
-      tabs.newDraft({ server: server.key, directory: sdk().directory }, search.prompt)
-    })
+  if (params.id && settings.general.newLayoutDesigns()) {
+    return <Navigate href={sessionHref(server.key, params.id)} />
+  }
 
-    return (
-      <SessionProviders>
-        <Session />
-      </SessionProviders>
-    )
-  },
-  { preload: Session.preload },
-)
-
-const TargetSessionRoute = Object.assign(
-  () => {
-    const params = useParams<{ serverKey: string; id: string }>()
-    const server = useServer()
-    const conn = createMemo(() => {
-      const key = requireServerKey(params.serverKey)
-      return server.list.find((item) => ServerConnection.key(item) === key)
-    })
+  // When the new layout is enabled, the legacy new-session route (/:dir/session with no id)
+  // is replaced by a draft at /new-session?draftId=…
+  createEffect(() => {
+    if (!settings.general.newLayoutDesigns()) return
+    if (params.id || search.draftId) return
+    if (!tabs.ready() || !sdk().directory) return
+    tabs.newDraft({ server: server.key, directory: sdk().directory }, search.prompt)
+  })
 
-    return (
-      <Show when={`${params.serverKey}\0${params.id}`} keyed>
-        <ServerSDKProvider server={conn}>
-          <ServerSyncProvider server={conn}>
-            <ResolvedTargetSessionRoute />
-          </ServerSyncProvider>
-        </ServerSDKProvider>
-      </Show>
-    )
-  },
-  { preload: Session.preload },
-)
+  return (
+    <SessionProviders>
+      <Session />
+    </SessionProviders>
+  )
+}
+
+const TargetSessionRoute = () => {
+  const params = useParams<{ serverKey: string; id: string }>()
+  const server = useServer()
+  const conn = createMemo(() => {
+    const key = requireServerKey(params.serverKey)
+    return server.list.find((item) => ServerConnection.key(item) === key)
+  })
+
+  return (
+    <Show when={`${params.serverKey}\0${params.id}`} keyed>
+      <ServerSDKProvider server={conn}>
+        <ServerSyncProvider server={conn}>
+          <ResolvedTargetSessionRoute />
+        </ServerSyncProvider>
+      </ServerSDKProvider>
+    </Show>
+  )
+}
 
 function ResolvedTargetSessionRoute() {
   const params = useParams<{ serverKey: string; id: string }>()
   const settings = useSettings()
   const tabs = useTabs()
+  const global = useGlobal()
   const serverSDK = useServerSDK()
   const serverKey = createMemo(() => requireServerKey(params.serverKey))
-  const resolved = useQuery(() => ({
-    queryKey: [serverSDK().scope, "session-route", params.id] as const,
-    queryFn: async () => {
-      const session = (await serverSDK().client.session.get({ sessionID: params.id })).data!
+  const placement = createMemo(() => global.sessionPlacement.get(serverKey(), params.id))
+  const [resolved] = createResource(
+    () => {
+      if (placement()) return
+      return { id: params.id, sdk: serverSDK() }
+    },
+    async ({ id, sdk }) => {
+      const session = (await sdk.client.session.get({ sessionID: id })).data!
       const root = await rootSession(session, (sessionID) =>
-        serverSDK()
-          .client.session.get({ sessionID })
-          .then((result) => result.data!),
+        sdk.client.session.get({ sessionID }).then((result) => result.data!),
       )
-      return { session, rootID: root.id }
+      return global.sessionPlacement.set({
+        server: serverKey(),
+        leafID: session.id,
+        rootID: root.id,
+        directory: session.directory,
+      })
     },
-  }))
-  const directory = createMemo<string | undefined>((prev) => prev ?? resolved.data?.session.directory)
+  )
+  const directory = createMemo(() => placement()?.directory ?? resolved()?.directory)
   const targetDirectory = () => directory()!
 
   createEffect(() => {
-    const current = resolved.data
+    const current = placement() ?? resolved()
     if (!current) return
     tabs.addSessionTab({
       server: serverKey(),
@@ -151,9 +153,7 @@ function ResolvedTargetSessionRoute() {
           >
             <SDKProvider directory={targetDirectory}>
               <DirectoryDataProvider directory={targetDirectory} server={serverKey}>
-                <Show when={resolved.data && !resolved.isPlaceholderData}>
-                  <TargetSessionPage />
-                </Show>
+                <TargetSessionPage />
               </DirectoryDataProvider>
             </SDKProvider>
           </Show>

+ 52 - 12
packages/app/src/components/titlebar.tsx

@@ -2,6 +2,7 @@ import {
   createEffect,
   createMemo,
   createResource,
+  createRoot,
   createSignal,
   For,
   Match,
@@ -512,25 +513,64 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
                             )
                           }
 
-                          const sdk = createMemo(() => {
-                            const conn = server.list.find((s) => ServerConnection.key(s) === tab.server)
-                            if (!conn) return null
-                            const { sdk } = global.createServerCtx(conn)
-                            return sdk
+                          const serverCtx = createMemo(() => {
+                            const conn = server.list.find((item) => ServerConnection.key(item) === tab.server)
+                            return conn ? global.createServerCtx(conn) : undefined
                           })
-                          const [session] = createResource(
+                          const sdk = createMemo(() => serverCtx()?.sdk ?? null)
+                          const cachedSession = createMemo(() => {
+                            const placement = global.sessionPlacement.get(tab.server, tab.sessionId)
+                            const ctx = serverCtx()
+                            if (!placement || !ctx) return
+                            return ctx.sync
+                              .child(placement.directory, { bootstrap: false })[0]
+                              .session.find((session) => session.id === tab.sessionId)
+                          })
+
+                          const [loadedSession] = createResource(
                             () => {
+                              if (cachedSession()) return null
                               const id = tab.sessionId
-                              const _sdk = sdk()
-                              if (!_sdk) return null
-                              return { id, sdk: _sdk }
+                              const ctx = serverCtx()
+                              return ctx ? { id, ctx } : null
                             },
-                            ({ id, sdk }) =>
-                              sdk.client.session
+                            ({ id, ctx }) =>
+                              ctx.sdk.client.session
                                 .get({ sessionID: id })
-                                .then((x) => x.data)
+                                .then((x) => {
+                                  const session = x.data
+                                  if (!session) return
+                                  if (!session.parentID)
+                                    global.sessionPlacement.set({
+                                      server: tab.server,
+                                      leafID: session.id,
+                                      rootID: session.id,
+                                      directory: session.directory,
+                                    })
+                                  return session
+                                })
                                 .catch(() => undefined),
                           )
+                          const session = createMemo(() => cachedSession() ?? loadedSession())
+                          let prefetched = false
+
+                          createEffect(() => {
+                            const ctx = serverCtx()
+                            const sess = session()
+                            if (!ctx || !sess || prefetched) return
+                            prefetched = true
+                            createRoot((dispose) => {
+                              try {
+                                void ctx.sync
+                                  .createDirSyncContext(sess.directory)
+                                  .session.sync(sess.id)
+                                  .catch(() => {})
+                                  .finally(dispose)
+                              } catch {
+                                dispose()
+                              }
+                            })
+                          })
 
                           createEffect(() => {
                             if (tab.type !== "session") return

+ 1 - 1
packages/app/src/context/directory-sync.ts

@@ -187,7 +187,7 @@ export const createDirSyncContext = (
     return serverSync.child(directory)
   }
   const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
-  const initialMessagePageSize = 80
+  const initialMessagePageSize = 2
   const historyMessagePageSize = 200
   const inflight = new Map<string, Promise<void>>()
   const inflightDiff = new Map<string, Promise<void>>()

+ 3 - 0
packages/app/src/context/global.tsx

@@ -8,11 +8,13 @@ import { createServerSyncContext } from "./server-sync"
 import { getOwner } from "solid-js/web"
 import { QueryClient } from "@tanstack/solid-query"
 import type { ServerScope } from "@/utils/server-scope"
+import { createSessionPlacementStore } from "@/utils/session-placement"
 
 export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext({
   name: "Global",
   init: () => {
     const server = useServer()
+    const sessionPlacement = createSessionPlacementStore()
     const serverHealth = useServerHealth(
       () => server.list,
       () => true,
@@ -85,6 +87,7 @@ export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext(
           },
         },
       },
+      sessionPlacement,
       createServerCtx(conn: ServerConnection.Any) {
         return ensureServerCtx(conn)
       },

+ 7 - 1
packages/app/src/pages/directory-layout.tsx

@@ -11,6 +11,7 @@ import { decode64 } from "@/utils/base64"
 import { Schema } from "effect"
 import type { ServerConnection } from "@/context/server"
 import { sessionHref } from "@/utils/session-route"
+import { useGlobal } from "@/context/global"
 
 export function DirectoryDataProvider(
   props: ParentProps<{
@@ -23,6 +24,7 @@ export function DirectoryDataProvider(
   const navigate = useNavigate()
   const params = useParams()
   const sync = useSync()
+  const global = useGlobal()
   const directory = () => (typeof props.directory === "function" ? props.directory() : props.directory)
   const slug = createMemo(() => base64Encode(directory()))
   const href = (sessionID: string) => {
@@ -52,7 +54,11 @@ export function DirectoryDataProvider(
     <DataProvider
       data={sync().data}
       directory={directory()}
-      onNavigateToSession={(sessionID: string) => navigate(href(sessionID))}
+      onNavigateToSession={(sessionID: string) => {
+        const server = props.server?.()
+        if (server && params.id) global.sessionPlacement.inherit(server, params.id, sessionID)
+        navigate(href(sessionID))
+      }}
       onSessionHref={href}
     >
       <LocalProvider>{props.children}</LocalProvider>

+ 46 - 1
packages/app/src/pages/home.tsx

@@ -3,6 +3,7 @@ import {
   batch,
   createEffect,
   createMemo,
+  createRoot,
   For,
   Match,
   on,
@@ -58,6 +59,8 @@ import { ServerRowMenu } from "@/components/server/server-row-menu"
 import { ServerHealthIndicator } from "@/components/server/server-row"
 import { type ServerHealth } from "@/utils/server-health"
 import { Persist, persisted } from "@/utils/persist"
+import { useMarked } from "@opencode-ai/ui/context/marked"
+import { preloadMarkdown } from "@opencode-ai/ui/markdown-cache"
 
 const HOME_SESSION_LIMIT = 64
 const HOME_ROW_LAYOUT =
@@ -134,9 +137,10 @@ export function NewHome() {
   const server = useServer()
   const language = useLanguage()
   const global = useGlobal()
+  const tabs = useTabs()
   const command = useCommand()
   const notification = useNotification()
-  const tabs = useTabs()
+  const marked = useMarked()
   let focusSessionSearch: (() => void) | undefined
   const [state, setState] = createStore({
     search: "",
@@ -199,6 +203,41 @@ export function NewHome() {
   })
   const searchOpen = createMemo(() => state.searchFocused && search().length > 0)
   const groups = createMemo(() => groupSessions(records(), language))
+  const prefetched = new Set<string>()
+
+  createEffect(() => {
+    const ctx = focusedServerCtx()
+    if (!ctx) return
+    records()
+      .slice(0, 2)
+      .forEach((record) => {
+        const key = `${ServerConnection.key(focusedServer()!)}\0${record.session.id}`
+        if (prefetched.has(key)) return
+        prefetched.add(key)
+        createRoot((dispose) => {
+          try {
+            const directory = ctx.sync.createDirSyncContext(record.session.directory)
+            void directory.session
+              .sync(record.session.id)
+              .then(() => {
+                const store = ctx.sync.child(record.session.directory)[0]
+                return Promise.all(
+                  (store.message[record.session.id] ?? []).flatMap((message) =>
+                    (store.part[message.id] ?? []).flatMap((part) => {
+                      if (part.type !== "text" || !part.text) return []
+                      return preloadMarkdown(part.text, part.id, marked)
+                    }),
+                  ),
+                )
+              })
+              .catch(() => {})
+              .finally(dispose)
+          } catch {
+            dispose()
+          }
+        })
+      })
+  })
 
   function setSelection(next: HomeProjectSelection) {
     batch(() => {
@@ -304,6 +343,12 @@ export function NewHome() {
     if (!conn) return
     const directory = project?.worktree ?? session.directory
     const ctx = global.createServerCtx(conn)
+    global.sessionPlacement.set({
+      server: ServerConnection.key(conn),
+      leafID: session.id,
+      rootID: session.id,
+      directory: session.directory,
+    })
     ctx.projects.open(directory)
     ctx.projects.touch(directory)
     startTransition(() => {

+ 26 - 4
packages/app/src/pages/session.tsx

@@ -287,7 +287,7 @@ export default function Page() {
       })
     }
     return key
-  }, sessionKey())
+  })
 
   let reviewFrame: number | undefined
   let todoFrame: number | undefined
@@ -693,6 +693,7 @@ export default function Page() {
   }
 
   createEffect(() => {
+    if (!sync().project) return
     const list = changesOptions()
     if (list.includes(store.changes)) return
     const next = list[0]
@@ -1128,13 +1129,34 @@ export default function Page() {
 
   let captureHistoryAnchor = () => {}
   let restoreHistoryAnchor = (_done: boolean) => {}
-  const loadOlder = () =>
-    timeline.history.loadOlder({ before: () => captureHistoryAnchor(), after: restoreHistoryAnchor })
+  let historyRequest = false
+  let historyContinuationFrame: number | undefined
+  const loadOlder = async () => {
+    if (historyRequest || historyLoading()) return
+    historyRequest = true
+    const before = timeline.messages().length
+    try {
+      await timeline.history.loadOlder({ before: () => captureHistoryAnchor(), after: restoreHistoryAnchor })
+    } finally {
+      historyRequest = false
+    }
+    if (timeline.messages().length <= before) return
+    if (!autoScroll.userScrolled() || !scroller || scroller.scrollTop >= 200 || !historyMore()) return
+    if (historyContinuationFrame !== undefined) cancelAnimationFrame(historyContinuationFrame)
+    historyContinuationFrame = requestAnimationFrame(() => {
+      historyContinuationFrame = undefined
+      onHistoryScroll()
+    })
+  }
   const onHistoryScroll = () => {
-    if (!autoScroll.userScrolled() || !scroller || scroller.scrollTop >= 200) return
+    if (historyRequest || historyLoading() || !autoScroll.userScrolled() || !scroller || scroller.scrollTop >= 200) return
     void loadOlder()
   }
 
+  onCleanup(() => {
+    if (historyContinuationFrame !== undefined) cancelAnimationFrame(historyContinuationFrame)
+  })
+
   fill = () => {
     if (fillFrame !== undefined) return
 

+ 3 - 15
packages/app/src/pages/session/timeline/model.test.ts

@@ -15,37 +15,29 @@ describe("timeline model", () => {
     expect(selectVisibleUserMessages(users)).toBe(users)
   })
 
-  test("loads pages until a visible user turn is added", async () => {
-    let loaded = 10
-    let visible = 2
+  test("loads exactly one opaque cursor page", async () => {
     let calls = 0
     const anchors: Array<string | boolean> = []
 
     await loadOlderTimeline({
       sessionID: () => "ses_test",
-      loaded: () => loaded,
-      visible: () => visible,
       more: () => true,
       loading: () => false,
       loadMore: async () => {
         calls += 1
-        loaded += 3
-        if (calls === 2) visible += 1
       },
       before: () => anchors.push("before"),
       after: (done) => anchors.push("after", done),
     })
 
-    expect(calls).toBe(2)
-    expect(anchors).toEqual(["before", "after", false, "after", true])
+    expect(calls).toBe(1)
+    expect(anchors).toEqual(["before", "after", true])
   })
 
   test("stops when a page adds no raw messages", async () => {
     let calls = 0
     await loadOlderTimeline({
       sessionID: () => "ses_test",
-      loaded: () => 10,
-      visible: () => 2,
       more: () => true,
       loading: () => false,
       loadMore: async () => {
@@ -62,8 +54,6 @@ describe("timeline model", () => {
 
     await loadOlderTimeline({
       sessionID: () => sessionID,
-      loaded: () => 10,
-      visible: () => 2,
       more: () => true,
       loading: () => false,
       loadMore: async () => {
@@ -83,8 +73,6 @@ describe("timeline model", () => {
     await expect(
       loadOlderTimeline({
         sessionID: () => "ses_test",
-        loaded: () => 10,
-        visible: () => 2,
         more: () => true,
         loading: () => false,
         loadMore: async () => {

+ 6 - 22
packages/app/src/pages/session/timeline/model.ts

@@ -74,8 +74,6 @@ export function createTimelineModel(input: {
   const loadOlder = async (options?: { before?: () => void; after?: (done: boolean) => void }) => {
     return loadOlderTimeline({
       sessionID: input.sessionID,
-      loaded: () => messages().length,
-      visible: () => visibleUserMessages().length,
       more,
       loading,
       loadMore: (sessionID) => sync().session.history.loadMore(sessionID),
@@ -115,8 +113,6 @@ export function selectVisibleUserMessages(messages: UserMessage[], revertMessage
 
 export async function loadOlderTimeline(input: {
   sessionID: Accessor<string | undefined>
-  loaded: Accessor<number>
-  visible: Accessor<number>
   more: Accessor<boolean>
   loading: Accessor<boolean>
   loadMore: (sessionID: string) => Promise<void>
@@ -126,23 +122,11 @@ export async function loadOlderTimeline(input: {
   const id = input.sessionID()
   if (!id || !input.more() || input.loading()) return
 
-  // A history page may contain only assistant messages or user turns hidden by a revert boundary.
-  const beforeVisible = input.visible()
-  let loaded = input.loaded()
   input.before?.()
-  while (true) {
-    await input.loadMore(id).catch((error) => {
-      if (input.sessionID() === id) input.after?.(true)
-      throw error
-    })
-    if (input.sessionID() !== id) return
-
-    const nextLoaded = input.loaded()
-    const growth = input.visible() - beforeVisible
-    const raw = nextLoaded - loaded
-    loaded = nextLoaded
-    const done = growth > 0 || raw <= 0 || !input.more()
-    input.after?.(done)
-    if (done) return
-  }
+  await input.loadMore(id).catch((error) => {
+    if (input.sessionID() === id) input.after?.(true)
+    throw error
+  })
+  if (input.sessionID() !== id) return
+  input.after?.(true)
 }

+ 38 - 0
packages/app/src/utils/session-placement.test.ts

@@ -0,0 +1,38 @@
+import { describe, expect, test } from "bun:test"
+import { ServerConnection } from "@/context/server"
+import { createSessionPlacementStore } from "./session-placement"
+
+describe("session placement", () => {
+  const local = ServerConnection.Key.make("http://localhost:4096")
+  const remote = ServerConnection.Key.make("https://example.com")
+
+  test("aliases a leaf and root without crossing servers", () => {
+    const store = createSessionPlacementStore()
+    store.set({ server: local, leafID: "child", rootID: "root", directory: "/repo" })
+    store.set({ server: remote, leafID: "child", rootID: "other", directory: "/remote" })
+
+    expect(store.get(local, "child")).toEqual({ rootID: "root", directory: "/repo" })
+    expect(store.get(local, "root")).toEqual({ rootID: "root", directory: "/repo" })
+    expect(store.get(remote, "child")).toEqual({ rootID: "other", directory: "/remote" })
+  })
+
+  test("inherits known placement for in-app child navigation", () => {
+    const store = createSessionPlacementStore()
+    store.set({ server: local, leafID: "parent", rootID: "root", directory: "/repo" })
+
+    expect(store.inherit(local, "parent", "child")).toEqual({ rootID: "root", directory: "/repo" })
+    expect(store.get(local, "child")).toEqual({ rootID: "root", directory: "/repo" })
+    expect(store.inherit(local, "missing", "unknown")).toBeUndefined()
+  })
+
+  test("bounds retained placement aliases", () => {
+    const store = createSessionPlacementStore()
+    for (let index = 0; index < 300; index++) {
+      store.set({ server: local, leafID: `leaf-${index}`, rootID: `root-${index}`, directory: `/repo/${index}` })
+    }
+
+    expect(store.size()).toBeLessThanOrEqual(256)
+    expect(store.get(local, "leaf-0")).toBeUndefined()
+    expect(store.get(local, "leaf-299")).toEqual({ rootID: "root-299", directory: "/repo/299" })
+  })
+})

+ 41 - 0
packages/app/src/utils/session-placement.ts

@@ -0,0 +1,41 @@
+import { ServerConnection } from "@/context/server"
+
+export type SessionPlacement = {
+  rootID: string
+  directory: string
+}
+
+export function createSessionPlacementStore() {
+  const placements = new Map<string, SessionPlacement>()
+  const limit = 256
+  const key = (server: ServerConnection.Key, sessionID: string) => `${server}\0${sessionID}`
+  const write = (id: string, placement: SessionPlacement) => {
+    placements.delete(id)
+    placements.set(id, placement)
+    while (placements.size > limit) placements.delete(placements.keys().next().value!)
+  }
+
+  return {
+    get(server: ServerConnection.Key, sessionID: string) {
+      const id = key(server, sessionID)
+      const placement = placements.get(id)
+      if (placement) write(id, placement)
+      return placement
+    },
+    set(input: SessionPlacement & { server: ServerConnection.Key; leafID: string }) {
+      const placement = { rootID: input.rootID, directory: input.directory }
+      write(key(input.server, input.leafID), placement)
+      write(key(input.server, input.rootID), placement)
+      return placement
+    },
+    inherit(server: ServerConnection.Key, sourceID: string, leafID: string) {
+      const placement = placements.get(key(server, sourceID))
+      if (!placement) return
+      write(key(server, leafID), placement)
+      return placement
+    },
+    size() {
+      return placements.size
+    },
+  }
+}

+ 78 - 0
packages/ui/src/components/markdown-cache.tsx

@@ -0,0 +1,78 @@
+import { checksum } from "@opencode-ai/core/util/encode"
+import DOMPurify from "dompurify"
+import { project } from "./markdown-stream"
+
+export type MarkdownCacheEntry = {
+  raw: string
+  hash: string
+  html: string
+}
+
+const max = 200
+const cache = new Map<string, MarkdownCacheEntry>()
+const config = {
+  USE_PROFILES: { html: true, mathMl: true },
+  SANITIZE_NAMED_PROPS: true,
+  FORBID_TAGS: ["style"],
+  FORBID_CONTENTS: ["style", "script"],
+  ADD_TAGS: ["svg", "path"],
+  ADD_ATTR: ["d", "viewBox", "preserveAspectRatio", "xmlns", "target"],
+}
+
+if (typeof window !== "undefined" && DOMPurify.isSupported) {
+  DOMPurify.addHook("afterSanitizeAttributes", (node: Element) => {
+    if (!(node instanceof HTMLAnchorElement)) return
+    if (node.target !== "_blank") return
+
+    const rel = node.getAttribute("rel") ?? ""
+    const set = new Set(rel.split(/\s+/).filter(Boolean))
+    set.add("noopener")
+    set.add("noreferrer")
+    node.setAttribute("rel", Array.from(set).join(" "))
+  })
+}
+
+export function sanitizeMarkdown(html: string) {
+  if (!DOMPurify.isSupported) return ""
+  return DOMPurify.sanitize(html, config)
+}
+
+export function getCachedMarkdown(key: string) {
+  return cache.get(key)
+}
+
+export function touchCachedMarkdown(key: string, value: MarkdownCacheEntry) {
+  cache.delete(key)
+  cache.set(key, value)
+
+  if (cache.size <= max) return
+
+  const first = cache.keys().next().value
+  if (!first) return
+  cache.delete(first)
+}
+
+export async function preloadMarkdown(
+  text: string,
+  cacheKey: string,
+  parser: { parse(text: string): string | Promise<string> },
+) {
+  await Promise.all(
+    project(undefined, text, false).blocks.map(async (block, index) => {
+      if (block.mode === "code") return
+      const key = `${cacheKey}:${index}:${block.mode}`
+      const cached = getCachedMarkdown(key)
+      if (cached?.raw === block.raw) {
+        touchCachedMarkdown(key, cached)
+        return
+      }
+      const hash = checksum(block.raw)
+      if (!hash) return
+      touchCachedMarkdown(key, {
+        raw: block.raw,
+        hash,
+        html: sanitizeMarkdown(await Promise.resolve(parser.parse(block.src))),
+      })
+    }),
+  )
+}

+ 18 - 0
packages/ui/src/components/markdown-preload.test.ts

@@ -0,0 +1,18 @@
+import { expect, test } from "bun:test"
+import { preloadMarkdown } from "./markdown-cache"
+
+test("preloads completed markdown into the render cache", async () => {
+  const parsed: string[] = []
+  const parser = {
+    parse(text: string) {
+      parsed.push(text)
+      return `<p>${text}</p>`
+    },
+  }
+  const key = `markdown-preload-${crypto.randomUUID()}`
+
+  await preloadMarkdown("prepared response", key, parser)
+  await preloadMarkdown("prepared response", key, parser)
+
+  expect(parsed).toEqual(["prepared response"])
+})

+ 7 - 53
packages/ui/src/components/markdown.tsx

@@ -1,6 +1,5 @@
 import { useMarked } from "../context/marked"
 import { useI18n } from "../context/i18n"
-import DOMPurify from "dompurify"
 import morphdom from "morphdom"
 import { checksum } from "@opencode-ai/core/util/encode"
 import {
@@ -25,15 +24,10 @@ import {
 } from "./markdown-worker"
 import { markdownBlockKey, type MarkdownToken } from "./markdown-worker-protocol"
 import { shouldResetCodeTokens, type RenderedCodeState } from "./markdown-code-state"
-
-type Entry = {
-  raw: string
-  hash: string
-  html: string
-}
+import { getCachedMarkdown, sanitizeMarkdown, touchCachedMarkdown, type MarkdownCacheEntry } from "./markdown-cache"
 
 type RenderedBlock =
-  | (Entry & { key: string; mode: Exclude<Block["mode"], "code"> })
+  | (MarkdownCacheEntry & { key: string; mode: Exclude<Block["mode"], "code"> })
   | {
       key: string
       mode: "code"
@@ -51,42 +45,13 @@ type RenderResult = {
   blocks: RenderedBlock[]
 }
 
-const max = 200
-const cache = new Map<string, Entry>()
 const renderedCodeTokens = new WeakMap<HTMLDivElement, RenderedCodeState>()
 
-if (typeof window !== "undefined" && DOMPurify.isSupported) {
-  DOMPurify.addHook("afterSanitizeAttributes", (node: Element) => {
-    if (!(node instanceof HTMLAnchorElement)) return
-    if (node.target !== "_blank") return
-
-    const rel = node.getAttribute("rel") ?? ""
-    const set = new Set(rel.split(/\s+/).filter(Boolean))
-    set.add("noopener")
-    set.add("noreferrer")
-    node.setAttribute("rel", Array.from(set).join(" "))
-  })
-}
-
-const config = {
-  USE_PROFILES: { html: true, mathMl: true },
-  SANITIZE_NAMED_PROPS: true,
-  FORBID_TAGS: ["style"],
-  FORBID_CONTENTS: ["style", "script"],
-  ADD_TAGS: ["svg", "path"],
-  ADD_ATTR: ["d", "viewBox", "preserveAspectRatio", "xmlns", "target"],
-}
-
 const iconPaths = {
   copy: '<path d="M6.2513 6.24935V2.91602H17.0846V13.7493H13.7513M13.7513 6.24935V17.0827H2.91797V6.24935H13.7513Z" stroke="currentColor" stroke-linecap="round"/>',
   check: '<path d="M5 11.9657L8.37838 14.7529L15 5.83398" stroke="currentColor" stroke-linecap="square"/>',
 }
 
-function sanitize(html: string) {
-  if (!DOMPurify.isSupported) return ""
-  return DOMPurify.sanitize(html, config)
-}
-
 function escape(text: string) {
   return text
     .replace(/&/g, "&amp;")
@@ -283,17 +248,6 @@ function setupCodeCopy(root: HTMLDivElement, getLabels: () => CopyLabels) {
   }
 }
 
-function touch(key: string, value: Entry) {
-  cache.delete(key)
-  cache.set(key, value)
-
-  if (cache.size <= max) return
-
-  const first = cache.keys().next().value
-  if (!first) return
-  cache.delete(first)
-}
-
 function initialResult(text: string, key: string | undefined, projection: Projection, owner: string): RenderResult {
   if (!text) return { text, blocks: [] }
   const base = key ?? checksum(text)
@@ -301,7 +255,7 @@ function initialResult(text: string, key: string | undefined, projection: Projec
     const blocks = projection.blocks.flatMap((block, index) => {
       if (block.mode === "code") return []
       const cacheKey = `${base}:${index}:${block.mode}`
-      const cached = cache.get(cacheKey)
+      const cached = getCachedMarkdown(cacheKey)
       if (cached?.raw !== block.raw) return []
       return [{ key: `${owner}:${cacheKey}`, mode: block.mode, ...cached }]
     })
@@ -387,16 +341,16 @@ export function Markdown(
           }
 
           if (key) {
-            const cached = cache.get(key)
+            const cached = getCachedMarkdown(key)
             if (cached?.raw === block.raw) {
-              touch(key, cached)
+              touchCachedMarkdown(key, cached)
               return { key: blockKey, mode: block.mode, ...cached }
             }
           }
 
           const hash = checksum(block.raw)
-          const safe = sanitize(await Promise.resolve(marked.parse(block.src)))
-          if (key && hash) touch(key, { raw: block.raw, hash, html: safe })
+          const safe = sanitizeMarkdown(await Promise.resolve(marked.parse(block.src)))
+          if (key && hash) touchCachedMarkdown(key, { raw: block.raw, hash, html: safe })
           return { key: blockKey, mode: block.mode, raw: block.raw, hash: hash ?? "", html: safe }
         }),
       )