Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2060,6 +2060,22 @@ describe("MessagesTimeline work rows", () => {
await using screen = await renderElement(<MessagesTimeline {...props} />);
expect(screen.text()).toContain("Thinking");

await screen.rerender(
<MessagesTimeline
{...props}
timelineEntries={[
...props.timelineEntries,
work("completed-command", {
runId,
label: "Ran pwd",
itemType: "command_execution",
toolLifecycleStatus: "completed",
}),
]}
/>,
);
expect(screen.text()).not.toContain("Thinking");

await screen.rerender(
<MessagesTimeline {...props} latestRun={{ ...props.latestRun, turnEnded: true }} />,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -970,7 +970,7 @@ describe("deriveMessagesTimelineRows", () => {
const rows = applyWorkingWindow(deriveMessagesTimelineRows(running), running);
expect(rows.map((row) => row.id)).toEqual(["prompt", "working-indicator-row", "reply"]);
expect(rows.filter((row) => row.kind === "working")).toMatchObject([
{ createdAt: prompt.createdAt, window: { items: [], answering: streaming } },
{ createdAt: prompt.createdAt, window: { items: [], answering: true } },
]);
expect(rows.some((row) => row.kind === "turn-fold")).toBe(false);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/MessagesTimeline.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2859,7 +2859,7 @@ export function applyWorkingWindow(
failedTests: deriveFailedTests(items),
retryAttempt: deriveRetryAttempt(items),
stopping: interruptRequested && !interruptSettled,
answering: answer?.kind === "text" && answer.message.streaming,
answering: answer?.kind === "text",
historyOpen: historyOpenKey === key,
};
return [
Expand Down
20 changes: 3 additions & 17 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,6 @@ const TIMELINE_MAINTAIN_SCROLL_AT_END = {
},
} as const satisfies MaintainScrollAtEndOptions;
const EMPTY_RUN_IDS: ReadonlySet<RunId> = new Set();
const TIMELINE_MAINTAIN_SCROLL_AT_END_SMOOTH = {
...TIMELINE_MAINTAIN_SCROLL_AT_END,
animated: true,
} as const satisfies MaintainScrollAtEndOptions;

const CITED_HISTORY_LOADING: CitedHistory = { _tag: "loading" };

export interface MessagesTimelineHistoryControls {
Expand Down Expand Up @@ -469,7 +464,6 @@ const ConversationTimeline = memo(function ConversationTimeline({
);
const restoringThreadPosition = positionedThreadKey !== listIdentityKey;
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
const [settlingListIdentity, setSettlingListIdentity] = useState<string | null>(listIdentityKey);
const [playingAudioRows, setPlayingAudioRows] = useState<ReadonlyMap<HTMLAudioElement, string>>(
() => new Map(),
);
Expand All @@ -490,7 +484,6 @@ const ConversationTimeline = memo(function ConversationTimeline({
findOpenAtOpening: findOpen,
});
setPositionedThreadKey(null);
setSettlingListIdentity(listIdentityKey);
// A viewer pinned in another thread must not keep live follow off here,
// nor when that thread comes back.
setFullscreenAppRowId(null);
Expand Down Expand Up @@ -1087,12 +1080,7 @@ const ConversationTimeline = memo(function ConversationTimeline({
};
// Rows added, removed or rewrapped (chat width) change the content size,
// which moves the scroll position, the overflow, and the content lane width.
const handleContentSizeChanged = useStableCallback((laidOutData: unknown) => {
// The first laid-out content of a newly shown thread ends its settling; a
// frame the previous thread scheduled laid out the previous thread's rows.
if (laidOutData === rows) {
setSettlingListIdentity((current) => (current === listIdentityKey ? null : current));
}
const handleContentSizeChanged = useStableCallback(() => {
handleScroll();
});
// The content lane only changes width with the viewport or the chat width
Expand Down Expand Up @@ -1272,7 +1260,7 @@ const ConversationTimeline = memo(function ConversationTimeline({
scheduledData = laidOutData;
contentSizeFrame = requestAnimationFrame(() => {
contentSizeFrame = null;
handleContentSizeChanged(laidOutData);
handleContentSizeChanged();
});
};
const stopListeningToContentSize = list
Expand Down Expand Up @@ -1473,9 +1461,7 @@ const ConversationTimeline = memo(function ConversationTimeline({
workingHistoryOpen ||
workingHistoryCollapsing
? false
: isWorking && !prefersReducedMotion && settlingListIdentity === null
? TIMELINE_MAINTAIN_SCROLL_AT_END_SMOOTH
: TIMELINE_MAINTAIN_SCROLL_AT_END
: TIMELINE_MAINTAIN_SCROLL_AT_END
}
maintainVisibleContentPosition={
findActive ||
Expand Down
25 changes: 5 additions & 20 deletions apps/web/src/components/chat/TimelineWorkGroupRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,7 @@ import {
} from "./TimelineWorkEntryRow";
import { formatWorkingTimer, formatWorkingTimerNow } from "./TimelineWorkGroupRows.logic";
import { WorkLogButton, WorkLogList, WorkLogRow, WorkLogTarget, WorkLogVerb } from "./WorkLog";
import {
type WorktreeSetupSnapshot,
type ToolActivityIcon,
type OrchestrationV2TurnItem,
} from "@t3tools/contracts";
import { type WorktreeSetupSnapshot, type ToolActivityIcon } from "@t3tools/contracts";
import { liveThoughtLine } from "@t3tools/client-runtime/work-log/presentation";
import { resolveWorkGroupScrollAnchor } from "@t3tools/client-runtime/work-log/scroll-anchor";
import { observeResize } from "~/lib/observeResize";
Expand Down Expand Up @@ -970,7 +966,7 @@ function WorkingWindowView({
!turnEnded &&
!pendingRequest &&
!window.answering &&
!workingWindowItemIsLive(window.items.at(-1)) &&
window.items.length === 0 &&
!isCompacting &&
!isPreparingWorktree &&
!workingWindowHasActiveCompaction(window);
Expand All @@ -983,7 +979,9 @@ function WorkingWindowView({
aria-label="Work history"
className={cn(
// The gap under the bar sits inside the window, so its fade reaches the hairline.
"flex min-w-0 flex-col justify-end gap-1 overflow-clip pt-2 transition-[height] duration-200 ease-out motion-reduce:transition-none",
"flex min-w-0 flex-col justify-end gap-1 overflow-clip pt-2",
history !== null &&
"transition-[height] duration-200 ease-out motion-reduce:transition-none",
open
? "[height:calc-size(auto,size)]"
: "[height:calc-size(auto,min(size,220px))] not-supports-[height:calc-size(auto,size)]:max-h-[220px]",
Expand Down Expand Up @@ -1152,15 +1150,6 @@ function CollapsedWorkingWindow({
);
}

function workingWindowItemIsLive(item: WorkingWindowItem | undefined): boolean {
if (item === undefined) return false;
if (item.kind === "work") return item.entry.toolLifecycleStatus === "inProgress";
if (item.kind === "text") return Boolean(item.message.streaming);
return (item.row.subagents ?? [item.row.projectedItem]).some(({ item: member }) =>
subagentItemIsLive(member.status),
);
}

const WORKING_WINDOW_TAIL = 12;
const WORKING_WINDOW_COLLAPSED_HEIGHT = 220;

Expand Down Expand Up @@ -1243,10 +1232,6 @@ function WorkingBarSlot({
) : null;
}

function subagentItemIsLive(status: OrchestrationV2TurnItem["status"]): boolean {
return status === "pending" || status === "running" || status === "waiting";
}

function workingWindowHasActiveCompaction(window: WorkingWindow | undefined): boolean {
return (
window?.items.some(
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/components/chat/timelineViewport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,13 @@ describe("navigationReleasesFollow", () => {
).toBe(false);
});

it("releases on downward keys, touch and row clicks only away from the end", () => {
it("releases on downward keys and touch only away from the end", () => {
const down = { source: "key", key: "PageDown", direction: "down" } as const;
expect(navigationReleasesFollow(down, viewport(true, false))).toBe(false);
expect(navigationReleasesFollow(down, viewport(false, true))).toBe(true);
expect(navigationReleasesFollow({ source: "touch" }, viewport(true, false))).toBe(false);
expect(
navigationReleasesFollow({ source: "pointer", onScrollbar: false }, viewport(true, false)),
navigationReleasesFollow({ source: "pointer", onScrollbar: false }, viewport(true, true)),
).toBe(false);
expect(
navigationReleasesFollow({ source: "pointer", onScrollbar: true }, viewport(true, false)),
Expand All @@ -98,6 +98,6 @@ describe("navigationReleasesFollow", () => {
expect(
navigationReleasesFollow({ source: "command", releasesFollow: false }, viewport(true, true)),
).toBe(false);
expect(navigationReleasesFollow({ source: "scroll" }, viewport(false, false))).toBe(true);
expect(navigationReleasesFollow({ source: "scroll" }, viewport(false, false))).toBe(false);
});
});
4 changes: 2 additions & 2 deletions apps/web/src/components/chat/timelineViewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,13 @@ export function navigationReleasesFollow(
case "key":
return navigation.direction === "up" ? viewport.contentScrollsUp() : viewport.awayFromEnd();
case "pointer":
return navigation.onScrollbar ? viewport.contentScrollsUp() : viewport.awayFromEnd();
return navigation.onScrollbar && viewport.contentScrollsUp();
case "touch":
// Touch direction is not observable; an upward flick leaves the end
// band within its first few events.
return viewport.awayFromEnd();
case "scroll":
return true;
return false;
case "command":
return navigation.releasesFollow;
}
Expand Down
Loading