From 333948711d347973640211e236463a79cd5459c9 Mon Sep 17 00:00:00 2001
From: Adam <2363879+adamdotdevin@users.noreply.github.com>
Date: Mon, 24 Nov 2025 20:35:51 -0600
Subject: [PATCH] fix(desktop): content animations
---
packages/ui/src/components/session-turn.css | 94 ++++++++++++++++++++-
packages/ui/src/components/session-turn.tsx | 37 +++++---
packages/ui/src/hooks/create-seen.ts | 20 -----
packages/ui/src/hooks/index.ts | 1 -
4 files changed, 118 insertions(+), 34 deletions(-)
delete mode 100644 packages/ui/src/hooks/create-seen.ts
diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css
index 1dfb54c56..d2a3d618a 100644
--- a/packages/ui/src/components/session-turn.css
+++ b/packages/ui/src/components/session-turn.css
@@ -101,7 +101,99 @@
}
&[data-fade="true"] > * {
- animation: fade-up-text 0.3s ease-out forwards;
+ animation: fadeUp 0.4s ease-out forwards;
+ opacity: 0;
+
+ &:nth-child(1) {
+ animation-delay: 0.1s;
+ }
+ &:nth-child(2) {
+ animation-delay: 0.2s;
+ }
+ &:nth-child(3) {
+ animation-delay: 0.3s;
+ }
+ &:nth-child(4) {
+ animation-delay: 0.4s;
+ }
+ &:nth-child(5) {
+ animation-delay: 0.5s;
+ }
+ &:nth-child(6) {
+ animation-delay: 0.6s;
+ }
+ &:nth-child(7) {
+ animation-delay: 0.7s;
+ }
+ &:nth-child(8) {
+ animation-delay: 0.8s;
+ }
+ &:nth-child(9) {
+ animation-delay: 0.9s;
+ }
+ &:nth-child(10) {
+ animation-delay: 1s;
+ }
+ &:nth-child(11) {
+ animation-delay: 1.1s;
+ }
+ &:nth-child(12) {
+ animation-delay: 1.2s;
+ }
+ &:nth-child(13) {
+ animation-delay: 1.3s;
+ }
+ &:nth-child(14) {
+ animation-delay: 1.4s;
+ }
+ &:nth-child(15) {
+ animation-delay: 1.5s;
+ }
+ &:nth-child(16) {
+ animation-delay: 1.6s;
+ }
+ &:nth-child(17) {
+ animation-delay: 1.7s;
+ }
+ &:nth-child(18) {
+ animation-delay: 1.8s;
+ }
+ &:nth-child(19) {
+ animation-delay: 1.9s;
+ }
+ &:nth-child(20) {
+ animation-delay: 2s;
+ }
+ &:nth-child(21) {
+ animation-delay: 2.1s;
+ }
+ &:nth-child(22) {
+ animation-delay: 2.2s;
+ }
+ &:nth-child(23) {
+ animation-delay: 2.3s;
+ }
+ &:nth-child(24) {
+ animation-delay: 2.4s;
+ }
+ &:nth-child(25) {
+ animation-delay: 2.5s;
+ }
+ &:nth-child(26) {
+ animation-delay: 2.6s;
+ }
+ &:nth-child(27) {
+ animation-delay: 2.7s;
+ }
+ &:nth-child(28) {
+ animation-delay: 2.8s;
+ }
+ &:nth-child(29) {
+ animation-delay: 2.9s;
+ }
+ &:nth-child(30) {
+ animation-delay: 3s;
+ }
}
}
diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx
index 6e5e74fad..a7bd456a4 100644
--- a/packages/ui/src/components/session-turn.tsx
+++ b/packages/ui/src/components/session-turn.tsx
@@ -2,7 +2,7 @@ import { AssistantMessage } from "@opencode-ai/sdk"
import { useData } from "../context"
import { Binary } from "@opencode-ai/util/binary"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
-import { createEffect, createMemo, createSignal, For, Match, ParentProps, Show, Switch } from "solid-js"
+import { createEffect, createMemo, createSignal, For, Match, onMount, ParentProps, Show, Switch } from "solid-js"
import { DiffChanges } from "./diff-changes"
import { Typewriter } from "./typewriter"
import { Message } from "./message-part"
@@ -55,10 +55,11 @@ export function SessionTurn(
{(msg) => {
- const titleSeen = createMemo(() => true)
- const contentSeen = createMemo(() => true)
+ const titleKey = `app:seen:session:${props.sessionID}:${msg().id}:title`
+ const contentKey = `app:seen:session:${props.sessionID}:${msg().id}:content`
const [detailsExpanded, setDetailsExpanded] = createSignal(false)
- const [titled, setTitled] = createSignal(titleSeen())
+ const [titled, setTitled] = createSignal(true)
+ const [faded, setFaded] = createSignal(true)
const assistantMessages = createMemo(() => {
return messages()?.filter((m) => m.role === "assistant" && m.parentID == msg().id) as AssistantMessage[]
@@ -68,7 +69,7 @@ export function SessionTurn(
const parts = createMemo(() => data.part[msg().id])
const lastTextPart = createMemo(() =>
assistantMessageParts()
- .filter((p) => p.type === "text")
+ .filter((p) => p?.type === "text")
?.at(-1),
)
const hasToolPart = createMemo(() => assistantMessageParts().some((p) => p?.type === "tool"))
@@ -79,11 +80,23 @@ export function SessionTurn(
const lastTextPartShown = createMemo(() => !msg().summary?.body && (lastTextPart()?.text?.length ?? 0) > 0)
// allowing time for the animations to finish
- createEffect(() => {
- if (titleSeen()) return
- const title = msg().summary?.title
- if (title) setTimeout(() => setTitled(true), 10_000)
+ onMount(() => {
+ const titleSeen = sessionStorage.getItem(titleKey) === "true"
+ const contentSeen = sessionStorage.getItem(contentKey) === "true"
+
+ if (!titleSeen) {
+ setTitled(false)
+ const title = msg().summary?.title
+ if (title) setTimeout(() => setTitled(true), 10_000)
+ setTimeout(() => sessionStorage.setItem(titleKey, "true"), 1000)
+ }
+
+ if (!contentSeen) {
+ setFaded(false)
+ setTimeout(() => sessionStorage.setItem(contentKey, "true"), 1000)
+ }
})
+
createEffect(() => {
const completed = !messageWorking()
setTimeout(() => setCompleted(completed), 1200)
@@ -120,7 +133,7 @@ export function SessionTurn(
)}
@@ -201,14 +214,14 @@ export function SessionTurn(
const parts = createMemo(() => data.part[assistantMessage.id])
const last = createMemo(() =>
parts()
- .filter((p) => p.type === "text")
+ .filter((p) => p?.type === "text")
.at(-1),
)
if (lastTextPartShown() && lastTextPart()?.id === last()?.id) {
return (
p.id !== last()?.id)}
+ parts={parts().filter((p) => p?.id !== last()?.id)}
/>
)
}
diff --git a/packages/ui/src/hooks/create-seen.ts b/packages/ui/src/hooks/create-seen.ts
deleted file mode 100644
index c25726e75..000000000
--- a/packages/ui/src/hooks/create-seen.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { createSignal, onCleanup, onMount } from "solid-js"
-import { isServer } from "solid-js/web"
-
-export function createSeen(key: string, delay = 1000) {
- // 1. Initialize state based on storage (default to true on server to avoid flash)
- const storageKey = `app:seen:${key}`
- const [hasSeen] = createSignal(!isServer && sessionStorage.getItem(storageKey) === "true")
-
- onMount(() => {
- // 2. If we haven't seen it, mark it as seen for NEXT time
- if (!hasSeen()) {
- const timer = setTimeout(() => {
- sessionStorage.setItem(storageKey, "true")
- }, delay)
- onCleanup(() => clearTimeout(timer))
- }
- })
-
- return hasSeen
-}
diff --git a/packages/ui/src/hooks/index.ts b/packages/ui/src/hooks/index.ts
index 98b339cfa..7eef78091 100644
--- a/packages/ui/src/hooks/index.ts
+++ b/packages/ui/src/hooks/index.ts
@@ -1,2 +1 @@
export * from "./use-filtered-list"
-export * from "./create-seen"