From c949e5b390814348a2a86802d4c350e964864da6 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 7 Jan 2026 19:12:48 -0600 Subject: feat(app): incrementally render turns, markdown cache, lazily render diffs --- packages/app/src/pages/session.tsx | 321 ++++++++++++++++++++++++++----------- 1 file changed, 231 insertions(+), 90 deletions(-) (limited to 'packages/app/src') diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 99306a2e7..e71177730 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -309,11 +309,20 @@ export default function Page() { activeTerminalDraggable: undefined as string | undefined, expanded: {} as Record, messageId: undefined as string | undefined, + turnStart: 0, mobileTab: "session" as "session" | "review", newSessionWorktree: "main", promptHeight: 0, }) + const renderedUserMessages = createMemo(() => { + const msgs = visibleUserMessages() + const start = store.turnStart + if (start <= 0) return msgs + if (start >= msgs.length) return emptyUserMessages + return msgs.slice(start) + }, emptyUserMessages) + const newSessionWorktree = createMemo(() => { if (store.newSessionWorktree === "create") return "create" const project = sync.project @@ -758,6 +767,88 @@ export default function Page() { autoScroll.scrollRef(el) } + const turnInit = 20 + const turnBatch = 20 + let turnHandle: number | undefined + let turnIdle = false + + function cancelTurnBackfill() { + const handle = turnHandle + if (handle === undefined) return + turnHandle = undefined + + if (turnIdle && window.cancelIdleCallback) { + window.cancelIdleCallback(handle) + return + } + + clearTimeout(handle) + } + + function scheduleTurnBackfill() { + if (turnHandle !== undefined) return + if (store.turnStart <= 0) return + + if (window.requestIdleCallback) { + turnIdle = true + turnHandle = window.requestIdleCallback(() => { + turnHandle = undefined + backfillTurns() + }) + return + } + + turnIdle = false + turnHandle = window.setTimeout(() => { + turnHandle = undefined + backfillTurns() + }, 0) + } + + function backfillTurns() { + const start = store.turnStart + if (start <= 0) return + + const next = start - turnBatch + const nextStart = next > 0 ? next : 0 + + const el = scroller + if (!el) { + setStore("turnStart", nextStart) + scheduleTurnBackfill() + return + } + + const beforeTop = el.scrollTop + const beforeHeight = el.scrollHeight + + setStore("turnStart", nextStart) + + requestAnimationFrame(() => { + const delta = el.scrollHeight - beforeHeight + if (delta) el.scrollTop = beforeTop + delta + }) + + scheduleTurnBackfill() + } + + createEffect( + on( + () => [params.id, messagesReady()] as const, + ([id, ready]) => { + cancelTurnBackfill() + setStore("turnStart", 0) + if (!id || !ready) return + + const len = visibleUserMessages().length + const start = len > turnInit ? len - turnInit : 0 + setStore("turnStart", start) + scheduleTurnBackfill() + }, + { defer: true }, + ), + ) + createResizeObserver( () => promptDock, ({ height }) => { @@ -785,6 +876,21 @@ export default function Page() { const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => { setActiveMessage(message) + const msgs = visibleUserMessages() + const index = msgs.findIndex((m) => m.id === message.id) + if (index !== -1 && index < store.turnStart) { + setStore("turnStart", index) + scheduleTurnBackfill() + + requestAnimationFrame(() => { + const el = document.getElementById(anchor(message.id)) + if (el) el.scrollIntoView({ behavior, block: "start" }) + }) + + updateHash(message.id) + return + } + const el = document.getElementById(anchor(message.id)) if (el) el.scrollIntoView({ behavior, block: "start" }) updateHash(message.id) @@ -830,12 +936,27 @@ export default function Page() { if (!sessionID || !ready) return requestAnimationFrame(() => { - const id = window.location.hash.slice(1) - const hashTarget = id ? document.getElementById(id) : undefined + const hash = window.location.hash.slice(1) + if (!hash) { + autoScroll.forceScrollToBottom() + return + } + + const hashTarget = document.getElementById(hash) if (hashTarget) { hashTarget.scrollIntoView({ behavior: "auto", block: "start" }) return } + + const match = hash.match(/^message-(.+)$/) + if (match) { + const msg = visibleUserMessages().find((m) => m.id === match[1]) + if (msg) { + scrollToMessage(msg, "auto") + return + } + } + autoScroll.forceScrollToBottom() }) }) @@ -868,6 +989,7 @@ export default function Page() { return [[path, file.selectedLines(path) ?? null] as const] }), ) + cancelTurnBackfill() document.removeEventListener("keydown", handleKeyDown) if (scrollSpyFrame !== undefined) cancelAnimationFrame(scrollSpyFrame) }) @@ -971,6 +1093,18 @@ export default function Page() { "mt-0": showTabs(), }} > + 0}> +
+ +
+
- + {(message) => { if (import.meta.env.DEV) { onMount(() => { @@ -1173,36 +1308,40 @@ export default function Page() { -
- Loading changes...
} - > - { - const value = file.tab(path) - tabs().open(value) - file.load(path) - }} - /> -
- + +
+ Loading changes...
} + > + { + const value = file.tab(path) + tabs().open(value) + file.load(path) + }} + /> +
+ + -
- -
+ +
+ +
+
@@ -1349,37 +1488,63 @@ export default function Page() { }} onScroll={handleScroll} > - - {(sel) => ( - - )} - - - -
- {path()} -
-
- -
+ + + {(sel) => ( + + )} + + + +
+ {path()} +
+
+ +
+ { + const p = path() + if (!p) return + file.setSelectedLines(p, range) + }} + overflow="scroll" + class="select-text" + /> + +
+ {path()} +
+
+
+
+ - -
- {path()} -
-
-
-
- - { - const p = path() - if (!p) return - file.setSelectedLines(p, range) - }} - overflow="scroll" - class="select-text pb-40" - /> - - -
Loading...
-
- - {(err) =>
{err()}
} -
-
+ + +
Loading...
+
+ + {(err) =>
{err()}
} +
+ + ) }} -- cgit v1.2.3