summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-03-02 18:23:59 -0600
committerAdam <[email protected]>2026-03-03 05:35:15 -0600
commit5e8742f4312a8923f3da92172a7247470ef34516 (patch)
tree8b9a664275be2d0b009359677f7826d1c486e38a
parent18850c4f911aaeb73846220e1139c1d07113b306 (diff)
downloadopencode-5e8742f4312a8923f3da92172a7247470ef34516.tar.gz
opencode-5e8742f4312a8923f3da92172a7247470ef34516.zip
fix(app): timeline jank
-rw-r--r--packages/app/src/pages/session.tsx4
-rw-r--r--packages/app/src/pages/session/message-timeline.tsx4
-rw-r--r--packages/ui/src/components/message-part.tsx170
-rw-r--r--packages/ui/src/hooks/create-auto-scroll.tsx5
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) => {