Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
85020e3
Migrate chat scrolling to LegendList
juliusmarminge Apr 12, 2026
175e3a8
Fix branch selector scroll reset
juliusmarminge Apr 12, 2026
083a86a
Remove stale timeline height parity tests
juliusmarminge Apr 12, 2026
f34a717
Remove thread mount auto-scroll from ChatView
juliusmarminge Apr 12, 2026
64275d4
Merge branch 'main' into t3code/legend-list-chat-scroll
juliusmarminge Apr 12, 2026
1620842
fix: skip scroll-based fetch effect in virtualized branch list path
cursoragent Apr 12, 2026
34c8c65
Fix scroll-to-bottom button not reset on thread switch
cursoragent Apr 12, 2026
85616f1
Disable animated chat autoscroll
juliusmarminge Apr 12, 2026
81d0901
Stabilize chat timeline scrolling and row updates
juliusmarminge Apr 12, 2026
93552f6
Remove react-scan from web shell
juliusmarminge Apr 12, 2026
fb6908b
Fix chat auto-scroll and bottom pill timing
juliusmarminge Apr 12, 2026
ae41ffe
Scroll chat to end before adding optimistic messages
juliusmarminge Apr 13, 2026
0cdb025
Merge branch 'main' into t3code/legend-list-chat-scroll
juliusmarminge Apr 13, 2026
97ca347
Tighten chat timeline markup
juliusmarminge Apr 13, 2026
f91e846
fix: use anyChanged flag to preserve array referential identity in us…
cursoragent Apr 13, 2026
349beb0
Remove unused groupId prop from WorkGroupSection
cursoragent Apr 13, 2026
c74144d
Use `use` for timeline row context
juliusmarminge Apr 13, 2026
eb77432
Surface turn summaries in chat timeline rows
juliusmarminge Apr 13, 2026
ae4c4c7
Keep branch picker anchored on keyboard navigation
juliusmarminge Apr 13, 2026
7e4ad44
Stabilize chat timeline row reuse
juliusmarminge Apr 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@dnd-kit/utilities": "^3.2.2",
"@effect/atom-react": "catalog:",
"@formkit/auto-animate": "^0.9.0",
"@legendapp/list": "3.0.0-beta.44",
"@lexical/react": "^0.41.0",
"@pierre/diffs": "^1.1.0-beta.16",
"@t3tools/client-runtime": "workspace:*",
Expand All @@ -29,7 +30,6 @@
"@tanstack/react-pacer": "^0.19.4",
"@tanstack/react-query": "^5.90.0",
"@tanstack/react-router": "^1.160.2",
"@tanstack/react-virtual": "^3.13.18",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
"class-variance-authority": "^0.7.1",
Expand Down
62 changes: 0 additions & 62 deletions apps/web/src/chat-scroll.test.ts

This file was deleted.

24 changes: 0 additions & 24 deletions apps/web/src/chat-scroll.ts

This file was deleted.

112 changes: 43 additions & 69 deletions apps/web/src/components/BranchToolbarBranchSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime";
import type { EnvironmentId, GitBranch, ThreadId } from "@t3tools/contracts";
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { useVirtualizer } from "@tanstack/react-virtual";
import { LegendList, type LegendListRef } from "@legendapp/list/react";
import { ChevronDownIcon } from "lucide-react";
import {
type CSSProperties,
useCallback,
useDeferredValue,
useEffect,
Expand Down Expand Up @@ -38,6 +37,7 @@ import {
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxListVirtualized,
ComboboxPopup,
ComboboxStatus,
ComboboxTrigger,
Expand Down Expand Up @@ -390,7 +390,7 @@ export function BranchToolbarBranchSelector({
}, [activeThreadBranch, activeWorktreePath, currentGitBranch, effectiveEnvMode, setThreadBranch]);

// ---------------------------------------------------------------------------
// Combobox / virtualizer plumbing
// Combobox / list plumbing
// ---------------------------------------------------------------------------
const handleOpenChange = useCallback(
(open: boolean) => {
Expand Down Expand Up @@ -425,49 +425,22 @@ export function BranchToolbarBranchSelector({

void fetchNextPage().catch(() => undefined);
}, [fetchNextPage, hasNextPage, isBranchMenuOpen, isFetchingNextPage]);
const branchListVirtualizer = useVirtualizer({
count: filteredBranchPickerItems.length,
estimateSize: (index) =>
filteredBranchPickerItems[index] === checkoutPullRequestItemValue ? 44 : 28,
getScrollElement: () => branchListScrollElementRef.current,
overscan: 12,
enabled: isBranchMenuOpen && shouldVirtualizeBranchList,
initialRect: {
height: 224,
width: 0,
},
});
const virtualBranchRows = branchListVirtualizer.getVirtualItems();
const setBranchListRef = useCallback(
(element: HTMLDivElement | null) => {
branchListScrollElementRef.current =
(element?.parentElement as HTMLDivElement | null) ?? null;
if (element) {
branchListVirtualizer.measure();
}
},
[branchListVirtualizer],
);

useEffect(() => {
if (!isBranchMenuOpen || !shouldVirtualizeBranchList) return;
queueMicrotask(() => {
branchListVirtualizer.measure();
});
}, [
branchListVirtualizer,
filteredBranchPickerItems.length,
isBranchMenuOpen,
shouldVirtualizeBranchList,
]);
const branchListRef = useRef<LegendListRef | null>(null);
const setBranchListRef = useCallback((element: HTMLDivElement | null) => {
branchListScrollElementRef.current = (element?.parentElement as HTMLDivElement | null) ?? null;
}, []);

useEffect(() => {
if (!isBranchMenuOpen) {
return;
}

branchListScrollElementRef.current?.scrollTo({ top: 0 });
}, [deferredTrimmedBranchQuery, isBranchMenuOpen]);
if (shouldVirtualizeBranchList) {
branchListRef.current?.scrollToOffset?.({ offset: 0, animated: false });
} else {
branchListScrollElementRef.current?.scrollTo({ top: 0 });
}
}, [deferredTrimmedBranchQuery, isBranchMenuOpen, shouldVirtualizeBranchList]);

useEffect(() => {
const scrollElement = branchListScrollElementRef.current;
Expand All @@ -487,24 +460,24 @@ export function BranchToolbarBranchSelector({
}, [isBranchMenuOpen, maybeFetchNextBranchPage]);

useEffect(() => {
if (shouldVirtualizeBranchList) return;
maybeFetchNextBranchPage();
}, [branches.length, maybeFetchNextBranchPage]);
}, [branches.length, maybeFetchNextBranchPage, shouldVirtualizeBranchList]);

const triggerLabel = getBranchTriggerLabel({
activeWorktreePath,
effectiveEnvMode,
resolvedActiveBranch,
});

function renderPickerItem(itemValue: string, index: number, style?: CSSProperties) {
function renderPickerItem(itemValue: string, index: number) {
if (checkoutPullRequestItemValue && itemValue === checkoutPullRequestItemValue) {
return (
<ComboboxItem
hideIndicator
key={itemValue}
index={index}
value={itemValue}
style={style}
onClick={() => {
if (!prReference || !onCheckoutPullRequestRequest) {
return;
Expand All @@ -529,7 +502,6 @@ export function BranchToolbarBranchSelector({
key={itemValue}
index={index}
value={itemValue}
style={style}
onClick={() => createBranch(trimmedBranchQuery)}
>
<span className="truncate">Create new branch &quot;{trimmedBranchQuery}&quot;</span>
Expand Down Expand Up @@ -557,7 +529,6 @@ export function BranchToolbarBranchSelector({
key={itemValue}
index={index}
value={itemValue}
style={style}
onClick={() => selectBranch(branch)}
>
<div className="flex w-full items-center justify-between gap-2">
Expand All @@ -576,7 +547,10 @@ export function BranchToolbarBranchSelector({
virtualized={shouldVirtualizeBranchList}
onItemHighlighted={(_value, eventDetails) => {
if (!isBranchMenuOpen || eventDetails.index < 0) return;
branchListVirtualizer.scrollToIndex(eventDetails.index, { align: "auto" });
branchListRef.current?.scrollIndexIntoView?.({
index: eventDetails.index,
animated: false,
});
}}
onOpenChange={handleOpenChange}
open={isBranchMenuOpen}
Expand Down Expand Up @@ -604,30 +578,30 @@ export function BranchToolbarBranchSelector({
</div>
<ComboboxEmpty>No branches found.</ComboboxEmpty>

<ComboboxList ref={setBranchListRef} className="max-h-56">
{shouldVirtualizeBranchList ? (
<div
className="relative"
style={{
height: `${branchListVirtualizer.getTotalSize()}px`,
{shouldVirtualizeBranchList ? (
<ComboboxListVirtualized>
<LegendList<string>
ref={branchListRef}
data={filteredBranchPickerItems}
keyExtractor={(item) => item}
renderItem={({ item, index }) => renderPickerItem(item, index)}
estimatedItemSize={28}
drawDistance={336}
onEndReached={() => {
if (hasNextPage && !isFetchingNextPage) {
void fetchNextPage().catch(() => undefined);
}
}}
>
{virtualBranchRows.map((virtualRow) => {
const itemValue = filteredBranchPickerItems[virtualRow.index];
if (!itemValue) return null;
return renderPickerItem(itemValue, virtualRow.index, {
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
});
})}
</div>
) : (
filteredBranchPickerItems.map((itemValue, index) => renderPickerItem(itemValue, index))
)}
</ComboboxList>
style={{ maxHeight: "14rem" }}
/>
</ComboboxListVirtualized>
) : (
<ComboboxList ref={setBranchListRef} className="max-h-56">
{filteredBranchPickerItems.map((itemValue, index) =>
renderPickerItem(itemValue, index),
)}
</ComboboxList>
)}
{branchStatusText ? <ComboboxStatus>{branchStatusText}</ComboboxStatus> : null}
</ComboboxPopup>
</Combobox>
Expand Down
Loading
Loading