diff options
| author | Adam <[email protected]> | 2026-03-02 18:23:59 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-03-03 05:35:15 -0600 |
| commit | 5e8742f4312a8923f3da92172a7247470ef34516 (patch) | |
| tree | 8b9a664275be2d0b009359677f7826d1c486e38a | |
| parent | 18850c4f911aaeb73846220e1139c1d07113b306 (diff) | |
| download | opencode-5e8742f4312a8923f3da92172a7247470ef34516.tar.gz opencode-5e8742f4312a8923f3da92172a7247470ef34516.zip | |
fix(app): timeline jank
| -rw-r--r-- | packages/app/src/pages/session.tsx | 4 | ||||
| -rw-r--r-- | packages/app/src/pages/session/message-timeline.tsx | 4 | ||||
| -rw-r--r-- | packages/ui/src/components/message-part.tsx | 170 | ||||
| -rw-r--r-- | packages/ui/src/hooks/create-auto-scroll.tsx | 5 |
4 files changed, 114 insertions, 69 deletions
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 4f01badf4..2c49489ba 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1091,7 +1091,9 @@ export default function Page() { const el = scroller const delta = next - dockHeight - const stick = el ? el.scrollHeight - el.clientHeight - el.scrollTop < 10 + Math.max(0, delta) : false + const stick = el + ? !autoScroll.userScrolled() || el.scrollHeight - el.clientHeight - el.scrollTop < 10 + Math.max(0, delta) + : false dockHeight = next diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 0aa07bf74..19d6e09d9 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -689,7 +689,9 @@ export function MessageTimeline(props: { if (!item || active()) return false return messageID > item.id }) - const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? [])) + const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? []), [], { + equals: (a, b) => JSON.stringify(a) === JSON.stringify(b), + }) const commentCount = createMemo(() => comments().length) return ( <div diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index b59dd47b8..2490f5c17 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -9,6 +9,7 @@ import { Show, Switch, onCleanup, + Index, type JSX, } from "solid-js" import stripAnsi from "strip-ansi" @@ -458,50 +459,67 @@ export function AssistantParts(props: { const last = createMemo(() => grouped().at(-1)?.key) return ( - <For each={grouped()}> - {(entry) => { - if (entry.type === "context") { - const parts = createMemo( - () => - entry.refs - .map((ref) => partByID(list(data.store.part?.[ref.messageID], emptyParts), ref.partID)) - .filter((part): part is ToolPart => !!part && isContextGroupTool(part)), - emptyTools, - { equals: same }, - ) - const busy = createMemo(() => props.working && last() === entry.key) - - return ( - <Show when={parts().length > 0}> - <ContextToolGroup parts={parts()} busy={busy()} /> - </Show> - ) - } - - const message = createMemo(() => props.messages.find((item) => item.id === entry.ref.messageID)) - const part = createMemo(() => - partByID(list(data.store.part?.[entry.ref.messageID], emptyParts), entry.ref.partID), - ) + <Index each={grouped()}> + {(entryAccessor) => { + const entryType = createMemo(() => entryAccessor().type) return ( - <Show when={message()}> - {(message) => ( - <Show when={part()}> - {(part) => ( - <Part - part={part()} - message={message()} - showAssistantCopyPartID={props.showAssistantCopyPartID} - turnDurationMs={props.turnDurationMs} - defaultOpen={partDefaultOpen(part(), props.shellToolDefaultOpen, props.editToolDefaultOpen)} - /> - )} - </Show> - )} - </Show> + <Switch> + <Match when={entryType() === "context"}> + {(() => { + const parts = createMemo( + () => { + const entry = entryAccessor() as { type: "context"; refs: PartRef[] } + return entry.refs + .map((ref) => partByID(list(data.store.part?.[ref.messageID], emptyParts), ref.partID)) + .filter((part): part is ToolPart => !!part && isContextGroupTool(part)) + }, + emptyTools, + { equals: same }, + ) + const busy = createMemo(() => props.working && last() === entryAccessor().key) + + return ( + <Show when={parts().length > 0}> + <ContextToolGroup parts={parts()} busy={busy()} /> + </Show> + ) + })()} + </Match> + <Match when={entryType() === "part"}> + {(() => { + const message = createMemo(() => { + const entry = entryAccessor() as { type: "part"; ref: PartRef } + return props.messages.find((item) => item.id === entry.ref.messageID) + }) + const part = createMemo(() => { + const entry = entryAccessor() as { type: "part"; ref: PartRef } + return partByID(list(data.store.part?.[entry.ref.messageID], emptyParts), entry.ref.partID) + }) + + return ( + <Show when={message()}> + {(msg) => ( + <Show when={part()}> + {(p) => ( + <Part + part={p()} + message={msg()} + showAssistantCopyPartID={props.showAssistantCopyPartID} + turnDurationMs={props.turnDurationMs} + defaultOpen={partDefaultOpen(p(), props.shellToolDefaultOpen, props.editToolDefaultOpen)} + /> + )} + </Show> + )} + </Show> + ) + })()} + </Match> + </Switch> ) }} - </For> + </Index> ) } @@ -632,36 +650,56 @@ export function AssistantMessageDisplay(props: { ) return ( - <For each={grouped()}> - {(entry) => { - if (entry.type === "context") { - const parts = createMemo( - () => - entry.refs - .map((ref) => partByID(props.parts, ref.partID)) - .filter((part): part is ToolPart => !!part && isContextGroupTool(part)), - emptyTools, - { equals: same }, - ) - - return ( - <Show when={parts().length > 0}> - <ContextToolGroup parts={parts()} /> - </Show> - ) - } - - const part = createMemo(() => partByID(props.parts, entry.ref.partID)) + <Index each={grouped()}> + {(entryAccessor) => { + const entryType = createMemo(() => entryAccessor().type) return ( - <Show when={part()}> - {(part) => ( - <Part part={part()} message={props.message} showAssistantCopyPartID={props.showAssistantCopyPartID} /> - )} - </Show> + <Switch> + <Match when={entryType() === "context"}> + {(() => { + const parts = createMemo( + () => { + const entry = entryAccessor() as { type: "context"; refs: PartRef[] } + return entry.refs + .map((ref) => partByID(props.parts, ref.partID)) + .filter((part): part is ToolPart => !!part && isContextGroupTool(part)) + }, + emptyTools, + { equals: same }, + ) + + return ( + <Show when={parts().length > 0}> + <ContextToolGroup parts={parts()} /> + </Show> + ) + })()} + </Match> + <Match when={entryType() === "part"}> + {(() => { + const part = createMemo(() => { + const entry = entryAccessor() as { type: "part"; ref: PartRef } + return partByID(props.parts, entry.ref.partID) + }) + + return ( + <Show when={part()}> + {(p) => ( + <Part + part={p()} + message={props.message} + showAssistantCopyPartID={props.showAssistantCopyPartID} + /> + )} + </Show> + ) + })()} + </Match> + </Switch> ) }} - </For> + </Index> ) } diff --git a/packages/ui/src/hooks/create-auto-scroll.tsx b/packages/ui/src/hooks/create-auto-scroll.tsx index c32017739..8483915a8 100644 --- a/packages/ui/src/hooks/create-auto-scroll.tsx +++ b/packages/ui/src/hooks/create-auto-scroll.tsx @@ -142,7 +142,10 @@ export function createAutoScroll(options: AutoScrollOptions) { const handleInteraction = () => { if (!active()) return - stop() + const selection = window.getSelection() + if (selection && selection.toString().length > 0) { + stop() + } } const updateOverflowAnchor = (el: HTMLElement) => { |
