fix(app): align virtual timeline scrolling
This commit is contained in:
parent
dcbe29c7c6
commit
240201b139
2 changed files with 29 additions and 33 deletions
|
|
@ -94,6 +94,9 @@ type TimelineRow =
|
||||||
| { key: string; type: "retry"; userMessageID: string }
|
| { key: string; type: "retry"; userMessageID: string }
|
||||||
| { key: string; type: "diff-summary"; userMessageID: string; diffs: SummaryDiff[] }
|
| { key: string; type: "diff-summary"; userMessageID: string; diffs: SummaryDiff[] }
|
||||||
| { key: string; type: "error"; userMessageID: string; text: string }
|
| { key: string; type: "error"; userMessageID: string; text: string }
|
||||||
|
| { key: string; type: "bottom-spacer" }
|
||||||
|
|
||||||
|
type FramedTimelineRow = Exclude<TimelineRow, { type: "bottom-spacer" }>
|
||||||
|
|
||||||
function sameKeys(a: readonly string[] | undefined, b: readonly string[] | undefined) {
|
function sameKeys(a: readonly string[] | undefined, b: readonly string[] | undefined) {
|
||||||
if (a === b) return true
|
if (a === b) return true
|
||||||
|
|
@ -129,6 +132,8 @@ function sameTimelineRow(a: TimelineRow, b: TimelineRow) {
|
||||||
if (a === b) return true
|
if (a === b) return true
|
||||||
if (a.key !== b.key) return false
|
if (a.key !== b.key) return false
|
||||||
if (a.type !== b.type) return false
|
if (a.type !== b.type) return false
|
||||||
|
if (a.type === "bottom-spacer") return b.type === "bottom-spacer"
|
||||||
|
if (b.type === "bottom-spacer") return false
|
||||||
if (a.userMessageID !== b.userMessageID) return false
|
if (a.userMessageID !== b.userMessageID) return false
|
||||||
|
|
||||||
switch (a.type) {
|
switch (a.type) {
|
||||||
|
|
@ -371,25 +376,6 @@ function TimelineDiffSummaryRow(props: { diffs: SummaryDiff[] }) {
|
||||||
<For each={visible()}>
|
<For each={visible()}>
|
||||||
{(diff) => {
|
{(diff) => {
|
||||||
const view = normalize(diff)
|
const view = normalize(diff)
|
||||||
const active = createMemo(() => expanded().includes(diff.file))
|
|
||||||
const [shown, setShown] = createSignal(false)
|
|
||||||
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
active,
|
|
||||||
(value) => {
|
|
||||||
if (!value) {
|
|
||||||
setShown(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (active()) setShown(true)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion.Item value={diff.file}>
|
<Accordion.Item value={diff.file}>
|
||||||
|
|
@ -414,11 +400,9 @@ function TimelineDiffSummaryRow(props: { diffs: SummaryDiff[] }) {
|
||||||
</Accordion.Trigger>
|
</Accordion.Trigger>
|
||||||
</StickyAccordionHeader>
|
</StickyAccordionHeader>
|
||||||
<Accordion.Content>
|
<Accordion.Content>
|
||||||
<Show when={shown()}>
|
<div data-slot="session-turn-diff-view" data-scrollable>
|
||||||
<div data-slot="session-turn-diff-view" data-scrollable>
|
<Dynamic component={fileComponent} mode="diff" fileDiff={view.fileDiff} />
|
||||||
<Dynamic component={fileComponent} mode="diff" fileDiff={view.fileDiff} />
|
</div>
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</Accordion.Content>
|
</Accordion.Content>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
)
|
)
|
||||||
|
|
@ -673,12 +657,17 @@ export function MessageTimeline(props: {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const timelineRows = createMemo(() => messageRowMemos().flatMap((memo) => memo()))
|
const timelineRows = createMemo((previous: TimelineRow[] | undefined) => {
|
||||||
|
const rows = messageRowMemos().flatMap((memo) => memo())
|
||||||
|
if (rows.length === 0) return rows
|
||||||
|
return reuseTimelineRows(previous, [...rows, { key: "bottom-spacer", type: "bottom-spacer" }])
|
||||||
|
})
|
||||||
const timelineRowKeys = createMemo(() => timelineRows().map((row) => row.key), [] as string[], { equals: sameKeys })
|
const timelineRowKeys = createMemo(() => timelineRows().map((row) => row.key), [] as string[], { equals: sameKeys })
|
||||||
const timelineRowByKey = createMemo(() => new Map(timelineRows().map((row) => [row.key, row] as const)))
|
const timelineRowByKey = createMemo(() => new Map(timelineRows().map((row) => [row.key, row] as const)))
|
||||||
const messageRowIndex = createMemo(() => {
|
const messageRowIndex = createMemo(() => {
|
||||||
const result = new Map<string, number>()
|
const result = new Map<string, number>()
|
||||||
timelineRows().forEach((row, index) => {
|
timelineRows().forEach((row, index) => {
|
||||||
|
if (!("userMessageID" in row)) return
|
||||||
if (result.has(row.userMessageID)) return
|
if (result.has(row.userMessageID)) return
|
||||||
result.set(row.userMessageID, index)
|
result.set(row.userMessageID, index)
|
||||||
})
|
})
|
||||||
|
|
@ -688,7 +677,7 @@ export function MessageTimeline(props: {
|
||||||
const id = activeMessageID()
|
const id = activeMessageID()
|
||||||
if (!id) return
|
if (!id) return
|
||||||
const rows = timelineRows()
|
const rows = timelineRows()
|
||||||
const index = rows.findLastIndex((row) => row.userMessageID === id)
|
const index = rows.findLastIndex((row) => "userMessageID" in row && row.userMessageID === id)
|
||||||
if (index < 0) return
|
if (index < 0) return
|
||||||
return [index]
|
return [index]
|
||||||
})
|
})
|
||||||
|
|
@ -1127,11 +1116,12 @@ export function MessageTimeline(props: {
|
||||||
showAssistantCopyPartID={assistantCopyPartID(row.userMessageID)}
|
showAssistantCopyPartID={assistantCopyPartID(row.userMessageID)}
|
||||||
turnDurationMs={turnDurationMs(row.userMessageID)}
|
turnDurationMs={turnDurationMs(row.userMessageID)}
|
||||||
defaultOpen={partDefaultOpen(part, settings.general.shellToolPartsExpanded(), settings.general.editToolPartsExpanded())}
|
defaultOpen={partDefaultOpen(part, settings.general.shellToolPartsExpanded(), settings.general.editToolPartsExpanded())}
|
||||||
|
deferToolContent={false}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function TimelineRowFrame(input: { row: TimelineRow; children: JSX.Element }) {
|
function TimelineRowFrame(input: { row: FramedTimelineRow; children: JSX.Element }) {
|
||||||
const anchor = () => input.row.type === "comment-strip" || (input.row.type === "user-message" && input.row.anchor)
|
const anchor = () => input.row.type === "comment-strip" || (input.row.type === "user-message" && input.row.anchor)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -1269,6 +1259,8 @@ export function MessageTimeline(props: {
|
||||||
</div>
|
</div>
|
||||||
</TimelineRowFrame>
|
</TimelineRowFrame>
|
||||||
)
|
)
|
||||||
|
case "bottom-spacer":
|
||||||
|
return <div data-timeline-row="bottom-spacer" aria-hidden="true" class="h-16" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1611,7 +1603,7 @@ export function MessageTimeline(props: {
|
||||||
ref={(handle) => {
|
ref={(handle) => {
|
||||||
virtualizer = handle
|
virtualizer = handle
|
||||||
}}
|
}}
|
||||||
class="relative min-w-0 w-full h-full pb-16 no-scrollbar"
|
class="relative min-w-0 w-full h-full"
|
||||||
onScroll={() => {
|
onScroll={() => {
|
||||||
const root = listRoot
|
const root = listRoot
|
||||||
if (!root) return
|
if (!root) return
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,7 @@ export interface MessagePartProps {
|
||||||
message: MessageType
|
message: MessageType
|
||||||
hideDetails?: boolean
|
hideDetails?: boolean
|
||||||
defaultOpen?: boolean
|
defaultOpen?: boolean
|
||||||
|
deferToolContent?: boolean
|
||||||
showAssistantCopyPartID?: string | null
|
showAssistantCopyPartID?: string | null
|
||||||
turnDurationMs?: number
|
turnDurationMs?: number
|
||||||
}
|
}
|
||||||
|
|
@ -1233,6 +1234,7 @@ export function Part(props: MessagePartProps) {
|
||||||
message={props.message}
|
message={props.message}
|
||||||
hideDetails={props.hideDetails}
|
hideDetails={props.hideDetails}
|
||||||
defaultOpen={props.defaultOpen}
|
defaultOpen={props.defaultOpen}
|
||||||
|
deferToolContent={props.deferToolContent}
|
||||||
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
||||||
turnDurationMs={props.turnDurationMs}
|
turnDurationMs={props.turnDurationMs}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1249,6 +1251,7 @@ export interface ToolProps {
|
||||||
status?: string
|
status?: string
|
||||||
hideDetails?: boolean
|
hideDetails?: boolean
|
||||||
defaultOpen?: boolean
|
defaultOpen?: boolean
|
||||||
|
deferContent?: boolean
|
||||||
forceOpen?: boolean
|
forceOpen?: boolean
|
||||||
locked?: boolean
|
locked?: boolean
|
||||||
}
|
}
|
||||||
|
|
@ -1387,6 +1390,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
||||||
status={part().state.status}
|
status={part().state.status}
|
||||||
hideDetails={props.hideDetails}
|
hideDetails={props.hideDetails}
|
||||||
defaultOpen={props.defaultOpen}
|
defaultOpen={props.defaultOpen}
|
||||||
|
deferContent={props.deferToolContent}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
@ -1888,7 +1892,7 @@ ToolRegistry.register({
|
||||||
<BasicTool
|
<BasicTool
|
||||||
{...props}
|
{...props}
|
||||||
icon="code-lines"
|
icon="code-lines"
|
||||||
defer
|
defer={props.deferContent !== false}
|
||||||
trigger={
|
trigger={
|
||||||
<div data-component="edit-trigger">
|
<div data-component="edit-trigger">
|
||||||
<div data-slot="message-part-title-area">
|
<div data-slot="message-part-title-area">
|
||||||
|
|
@ -1960,7 +1964,7 @@ ToolRegistry.register({
|
||||||
<BasicTool
|
<BasicTool
|
||||||
{...props}
|
{...props}
|
||||||
icon="code-lines"
|
icon="code-lines"
|
||||||
defer
|
defer={props.deferContent !== false}
|
||||||
trigger={
|
trigger={
|
||||||
<div data-component="write-trigger">
|
<div data-component="write-trigger">
|
||||||
<div data-slot="message-part-title-area">
|
<div data-slot="message-part-title-area">
|
||||||
|
|
@ -2042,7 +2046,7 @@ ToolRegistry.register({
|
||||||
<BasicTool
|
<BasicTool
|
||||||
{...props}
|
{...props}
|
||||||
icon="code-lines"
|
icon="code-lines"
|
||||||
defer
|
defer={props.deferContent !== false}
|
||||||
trigger={{
|
trigger={{
|
||||||
title: i18n.t("ui.tool.patch"),
|
title: i18n.t("ui.tool.patch"),
|
||||||
subtitle: subtitle(),
|
subtitle: subtitle(),
|
||||||
|
|
@ -2114,7 +2118,7 @@ ToolRegistry.register({
|
||||||
</Accordion.Trigger>
|
</Accordion.Trigger>
|
||||||
</StickyAccordionHeader>
|
</StickyAccordionHeader>
|
||||||
<Accordion.Content>
|
<Accordion.Content>
|
||||||
<Show when={visible()}>
|
<Show when={props.deferContent === false || visible()}>
|
||||||
<div data-component="apply-patch-file-diff">
|
<div data-component="apply-patch-file-diff">
|
||||||
<Dynamic component={fileComponent} mode="diff" fileDiff={file.view.fileDiff} />
|
<Dynamic component={fileComponent} mode="diff" fileDiff={file.view.fileDiff} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2134,7 +2138,7 @@ ToolRegistry.register({
|
||||||
<BasicTool
|
<BasicTool
|
||||||
{...props}
|
{...props}
|
||||||
icon="code-lines"
|
icon="code-lines"
|
||||||
defer
|
defer={props.deferContent !== false}
|
||||||
trigger={
|
trigger={
|
||||||
<div data-component="edit-trigger">
|
<div data-component="edit-trigger">
|
||||||
<div data-slot="message-part-title-area">
|
<div data-slot="message-part-title-area">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue