Skip to content

Commit 399b8f0

Browse files
authored
fix(app): session title turn spinner (#16764)
1 parent 3742e42 commit 399b8f0

3 files changed

Lines changed: 94 additions & 37 deletions

File tree

packages/app/src/pages/session/message-timeline.tsx

Lines changed: 91 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
88
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
99
import { Dialog } from "@opencode-ai/ui/dialog"
1010
import { InlineInput } from "@opencode-ai/ui/inline-input"
11+
import { Spinner } from "@opencode-ai/ui/spinner"
1112
import { SessionTurn } from "@opencode-ai/ui/session-turn"
1213
import { ScrollView } from "@opencode-ai/ui/scroll-view"
1314
import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
@@ -235,6 +236,40 @@ export function MessageTimeline(props: {
235236
if (!id) return idle
236237
return sync.data.session_status[id] ?? idle
237238
})
239+
const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
240+
241+
const [slot, setSlot] = createStore({
242+
open: false,
243+
show: false,
244+
fade: false,
245+
})
246+
247+
let f: number | undefined
248+
const clear = () => {
249+
if (f !== undefined) window.clearTimeout(f)
250+
f = undefined
251+
}
252+
253+
onCleanup(clear)
254+
createEffect(
255+
on(
256+
working,
257+
(on, prev) => {
258+
clear()
259+
if (on) {
260+
setSlot({ open: true, show: true, fade: false })
261+
return
262+
}
263+
if (prev) {
264+
setSlot({ open: false, show: true, fade: true })
265+
f = window.setTimeout(() => setSlot({ show: false, fade: false }), 260)
266+
return
267+
}
268+
setSlot({ open: false, show: false, fade: false })
269+
},
270+
{ defer: true },
271+
),
272+
)
238273
const activeMessageID = createMemo(() => {
239274
const parentID = pending()?.parentID
240275
if (parentID) {
@@ -573,43 +608,64 @@ export function MessageTimeline(props: {
573608
aria-label={language.t("common.goBack")}
574609
/>
575610
</Show>
576-
<Show when={titleValue() || title.editing}>
577-
<Show
578-
when={title.editing}
579-
fallback={
580-
<h1
581-
class="text-14-medium text-text-strong truncate grow-1 min-w-0 pl-2"
582-
onDblClick={openTitleEditor}
583-
>
584-
{titleValue()}
585-
</h1>
586-
}
611+
<div class="flex items-center min-w-0 grow-1">
612+
<div
613+
class="shrink-0 flex items-center justify-center overflow-hidden transition-[width,margin] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]"
614+
style={{
615+
width: slot.open ? "16px" : "0px",
616+
"margin-right": slot.open ? "8px" : "0px",
617+
}}
618+
aria-hidden="true"
587619
>
588-
<InlineInput
589-
ref={(el) => {
590-
titleRef = el
591-
}}
592-
value={title.draft}
593-
disabled={title.saving}
594-
class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]"
595-
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
596-
onInput={(event) => setTitle("draft", event.currentTarget.value)}
597-
onKeyDown={(event) => {
598-
event.stopPropagation()
599-
if (event.key === "Enter") {
600-
event.preventDefault()
601-
void saveTitleEditor()
602-
return
603-
}
604-
if (event.key === "Escape") {
605-
event.preventDefault()
606-
closeTitleEditor()
607-
}
608-
}}
609-
onBlur={closeTitleEditor}
610-
/>
620+
<Show when={slot.show}>
621+
<div
622+
class="transition-opacity duration-200 ease-out"
623+
classList={{
624+
"opacity-0": slot.fade,
625+
}}
626+
>
627+
<Spinner class="size-4" style={{ color: "var(--icon-interactive-base)" }} />
628+
</div>
629+
</Show>
630+
</div>
631+
<Show when={titleValue() || title.editing}>
632+
<Show
633+
when={title.editing}
634+
fallback={
635+
<h1
636+
class="text-14-medium text-text-strong truncate grow-1 min-w-0"
637+
onDblClick={openTitleEditor}
638+
>
639+
{titleValue()}
640+
</h1>
641+
}
642+
>
643+
<InlineInput
644+
ref={(el) => {
645+
titleRef = el
646+
}}
647+
value={title.draft}
648+
disabled={title.saving}
649+
class="text-14-medium text-text-strong grow-1 min-w-0 rounded-[6px]"
650+
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
651+
onInput={(event) => setTitle("draft", event.currentTarget.value)}
652+
onKeyDown={(event) => {
653+
event.stopPropagation()
654+
if (event.key === "Enter") {
655+
event.preventDefault()
656+
void saveTitleEditor()
657+
return
658+
}
659+
if (event.key === "Escape") {
660+
event.preventDefault()
661+
closeTitleEditor()
662+
}
663+
}}
664+
onBlur={closeTitleEditor}
665+
/>
666+
</Show>
611667
</Show>
612-
</Show>
668+
</div>
613669
</div>
614670
<Show when={sessionID()}>
615671
{(id) => (

packages/ui/src/components/spinner.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export function Spinner(props: {
4141
animation: square.corner
4242
? undefined
4343
: `${square.outer ? "pulse-opacity-dim" : "pulse-opacity"} ${square.duration}s ease-in-out infinite`,
44+
"animation-fill-mode": square.corner ? undefined : "both",
4445
"animation-delay": square.corner ? undefined : `${square.delay}s`,
4546
}}
4647
/>

packages/ui/src/styles/animations.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@
2626
@keyframes pulse-opacity-dim {
2727
0%,
2828
100% {
29-
opacity: 0;
29+
opacity: 0.15;
3030
}
3131
50% {
32-
opacity: 0.2;
32+
opacity: 0.35;
3333
}
3434
}
3535

0 commit comments

Comments
 (0)