mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-18 10:07:58 +00:00
fix(app): align virtual timeline scrolling
This commit is contained in:
@@ -94,6 +94,9 @@ type TimelineRow =
|
||||
| { key: string; type: "retry"; userMessageID: string }
|
||||
| { key: string; type: "diff-summary"; userMessageID: string; diffs: SummaryDiff[] }
|
||||
| { 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) {
|
||||
if (a === b) return true
|
||||
@@ -129,6 +132,8 @@ function sameTimelineRow(a: TimelineRow, b: TimelineRow) {
|
||||
if (a === b) return true
|
||||
if (a.key !== b.key) 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
|
||||
|
||||
switch (a.type) {
|
||||
@@ -371,25 +376,6 @@ function TimelineDiffSummaryRow(props: { diffs: SummaryDiff[] }) {
|
||||
<For each={visible()}>
|
||||
{(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 (
|
||||
<Accordion.Item value={diff.file}>
|
||||
@@ -414,11 +400,9 @@ function TimelineDiffSummaryRow(props: { diffs: SummaryDiff[] }) {
|
||||
</Accordion.Trigger>
|
||||
</StickyAccordionHeader>
|
||||
<Accordion.Content>
|
||||
<Show when={shown()}>
|
||||
<div data-slot="session-turn-diff-view" data-scrollable>
|
||||
<Dynamic component={fileComponent} mode="diff" fileDiff={view.fileDiff} />
|
||||
</div>
|
||||
</Show>
|
||||
<div data-slot="session-turn-diff-view" data-scrollable>
|
||||
<Dynamic component={fileComponent} mode="diff" fileDiff={view.fileDiff} />
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
</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 timelineRowByKey = createMemo(() => new Map(timelineRows().map((row) => [row.key, row] as const)))
|
||||
const messageRowIndex = createMemo(() => {
|
||||
const result = new Map<string, number>()
|
||||
timelineRows().forEach((row, index) => {
|
||||
if (!("userMessageID" in row)) return
|
||||
if (result.has(row.userMessageID)) return
|
||||
result.set(row.userMessageID, index)
|
||||
})
|
||||
@@ -688,7 +677,7 @@ export function MessageTimeline(props: {
|
||||
const id = activeMessageID()
|
||||
if (!id) return
|
||||
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
|
||||
return [index]
|
||||
})
|
||||
@@ -1127,11 +1116,12 @@ export function MessageTimeline(props: {
|
||||
showAssistantCopyPartID={assistantCopyPartID(row.userMessageID)}
|
||||
turnDurationMs={turnDurationMs(row.userMessageID)}
|
||||
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)
|
||||
|
||||
return (
|
||||
@@ -1269,6 +1259,8 @@ export function MessageTimeline(props: {
|
||||
</div>
|
||||
</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) => {
|
||||
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={() => {
|
||||
const root = listRoot
|
||||
if (!root) return
|
||||
|
||||
@@ -150,6 +150,7 @@ export interface MessagePartProps {
|
||||
message: MessageType
|
||||
hideDetails?: boolean
|
||||
defaultOpen?: boolean
|
||||
deferToolContent?: boolean
|
||||
showAssistantCopyPartID?: string | null
|
||||
turnDurationMs?: number
|
||||
}
|
||||
@@ -1233,6 +1234,7 @@ export function Part(props: MessagePartProps) {
|
||||
message={props.message}
|
||||
hideDetails={props.hideDetails}
|
||||
defaultOpen={props.defaultOpen}
|
||||
deferToolContent={props.deferToolContent}
|
||||
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
||||
turnDurationMs={props.turnDurationMs}
|
||||
/>
|
||||
@@ -1249,6 +1251,7 @@ export interface ToolProps {
|
||||
status?: string
|
||||
hideDetails?: boolean
|
||||
defaultOpen?: boolean
|
||||
deferContent?: boolean
|
||||
forceOpen?: boolean
|
||||
locked?: boolean
|
||||
}
|
||||
@@ -1387,6 +1390,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
||||
status={part().state.status}
|
||||
hideDetails={props.hideDetails}
|
||||
defaultOpen={props.defaultOpen}
|
||||
deferContent={props.deferToolContent}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
@@ -1888,7 +1892,7 @@ ToolRegistry.register({
|
||||
<BasicTool
|
||||
{...props}
|
||||
icon="code-lines"
|
||||
defer
|
||||
defer={props.deferContent !== false}
|
||||
trigger={
|
||||
<div data-component="edit-trigger">
|
||||
<div data-slot="message-part-title-area">
|
||||
@@ -1960,7 +1964,7 @@ ToolRegistry.register({
|
||||
<BasicTool
|
||||
{...props}
|
||||
icon="code-lines"
|
||||
defer
|
||||
defer={props.deferContent !== false}
|
||||
trigger={
|
||||
<div data-component="write-trigger">
|
||||
<div data-slot="message-part-title-area">
|
||||
@@ -2042,7 +2046,7 @@ ToolRegistry.register({
|
||||
<BasicTool
|
||||
{...props}
|
||||
icon="code-lines"
|
||||
defer
|
||||
defer={props.deferContent !== false}
|
||||
trigger={{
|
||||
title: i18n.t("ui.tool.patch"),
|
||||
subtitle: subtitle(),
|
||||
@@ -2114,7 +2118,7 @@ ToolRegistry.register({
|
||||
</Accordion.Trigger>
|
||||
</StickyAccordionHeader>
|
||||
<Accordion.Content>
|
||||
<Show when={visible()}>
|
||||
<Show when={props.deferContent === false || visible()}>
|
||||
<div data-component="apply-patch-file-diff">
|
||||
<Dynamic component={fileComponent} mode="diff" fileDiff={file.view.fileDiff} />
|
||||
</div>
|
||||
@@ -2134,7 +2138,7 @@ ToolRegistry.register({
|
||||
<BasicTool
|
||||
{...props}
|
||||
icon="code-lines"
|
||||
defer
|
||||
defer={props.deferContent !== false}
|
||||
trigger={
|
||||
<div data-component="edit-trigger">
|
||||
<div data-slot="message-part-title-area">
|
||||
|
||||
Reference in New Issue
Block a user