Reworks the chat and session-sidebar render paths to cut render cascades, memory
churn, and UI jank on large sessions and big session trees. Behavior is preserved;
the changes are about *when* and *how much* the UI re-renders.
## Chat streaming
- Freeze the streaming message's parts in the bulk turn projection during streaming,
and re-inject live parts only in an isolated tail leaf, so a ~60/sec delta stream
no longer re-runs the whole-session projection or re-renders unrelated rows.
session with referential reuse of unchanged turns.
- Memoize message rows with field-aware comparators instead of reference equality.
- Replace the manual child-session polling in the task tool with the live SSE
stream + a one-shot load, removing a fetch/settle state machine.
## History loading & scroll
- Load an initial page fast, then prepend one older page in the background so the
scroll container has headroom and "load older on scroll-up" fires before the user
hits the absolute top.
- Compensate scroll synchronously (in a layout effect, before paint) for prepends —
including background prepends that don't originate from a user scroll — so the
viewport stays stable instead of judder-correcting on the next frame.
## Markdown rendering
- Render markdown synchronously *styled* on first paint (paragraphs, lists, code
cards, tables, inline code) instead of raw escaped text; the async pass then only
upgrades syntax-highlight colors. Eliminates the flash of full-width raw text.
- Load KaTeX CSS eagerly with the main bundle instead of inside the lazy markdown
chunk, avoiding a late stylesheet injection on first render.
## Sidebar
- Hoist per-row recursive tree walks out of row comparators into per-group
precomputed sets/keys; batch live-session lookups into a single map; add a
group-level memo boundary.
- Isolate rename drafts so per-keystroke typing doesn't repaint the row tree.
## Sync layer
- Add a staleness guard so a slow message fetch can't repopulate a session the user
navigated away from.
- Throw on fetch failure for authoritative loaders so a transient blip can't read as
an empty server response.
## Cleanup
- Remove dead code (unused hooks, params, duplicated inline types) surfaced while
reworking the above.
## Known issue
- A rare, purely cosmetic first-paint width flash can still appear on large sessions;
it has no behavioral or data impact and is tracked for a follow-up runtime trace.
55 lines
1.9 KiB
TypeScript
55 lines
1.9 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import type { Session } from "@opencode-ai/sdk/v2"
|
|
|
|
import {
|
|
areRequestArraysReferentiallyEqual,
|
|
collectScopedBlockingRequests,
|
|
} from "./scoped-blocking-requests"
|
|
|
|
const session = (id: string, parentID?: string): Session => ({ id, parentID }) as Session
|
|
|
|
describe("scoped blocking requests", () => {
|
|
test("collects requests for the current session subtree", () => {
|
|
const rootRequest = { id: "perm_root" }
|
|
const childRequest = { id: "perm_child" }
|
|
const grandchildRequest = { id: "perm_grandchild" }
|
|
const siblingRequest = { id: "perm_sibling" }
|
|
const empty: Array<typeof rootRequest> = []
|
|
|
|
const result = collectScopedBlockingRequests(
|
|
[
|
|
session("ses_root"),
|
|
session("ses_child", "ses_root"),
|
|
session("ses_grandchild", "ses_child"),
|
|
session("ses_sibling"),
|
|
],
|
|
{
|
|
ses_root: [rootRequest],
|
|
ses_child: [childRequest],
|
|
ses_grandchild: [grandchildRequest],
|
|
ses_sibling: [siblingRequest],
|
|
},
|
|
"ses_root",
|
|
empty,
|
|
)
|
|
|
|
expect(result).toEqual([rootRequest, childRequest, grandchildRequest])
|
|
})
|
|
|
|
test("returns the provided empty array when no scoped requests exist", () => {
|
|
const empty: Array<{ id: string }> = []
|
|
|
|
expect(collectScopedBlockingRequests([session("ses_root")], {}, "ses_root", empty)).toBe(empty)
|
|
expect(collectScopedBlockingRequests([session("ses_root")], {}, null, empty)).toBe(empty)
|
|
})
|
|
|
|
test("compares request arrays by item identity", () => {
|
|
const first = { id: "perm_1" }
|
|
const second = { id: "perm_2" }
|
|
|
|
expect(areRequestArraysReferentiallyEqual([first, second], [first, second])).toBe(true)
|
|
expect(areRequestArraysReferentiallyEqual([first, second], [second, first])).toBe(false)
|
|
expect(areRequestArraysReferentiallyEqual([first], [{ id: "perm_1" }])).toBe(false)
|
|
})
|
|
})
|