fix(app): session title turn spinner (#16764)

This commit is contained in:
David Hill
2026-03-09 09:46:15 -05:00
committed by GitHub
parent 3742e42fdf
commit 399b8f0701
3 changed files with 94 additions and 37 deletions
@@ -8,6 +8,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Dialog } from "@opencode-ai/ui/dialog" import { Dialog } from "@opencode-ai/ui/dialog"
import { InlineInput } from "@opencode-ai/ui/inline-input" import { InlineInput } from "@opencode-ai/ui/inline-input"
import { Spinner } from "@opencode-ai/ui/spinner"
import { SessionTurn } from "@opencode-ai/ui/session-turn" import { SessionTurn } from "@opencode-ai/ui/session-turn"
import { ScrollView } from "@opencode-ai/ui/scroll-view" import { ScrollView } from "@opencode-ai/ui/scroll-view"
import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2" import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2"
@@ -235,6 +236,40 @@ export function MessageTimeline(props: {
if (!id) return idle if (!id) return idle
return sync.data.session_status[id] ?? idle return sync.data.session_status[id] ?? idle
}) })
const working = createMemo(() => !!pending() || sessionStatus().type !== "idle")
const [slot, setSlot] = createStore({
open: false,
show: false,
fade: false,
})
let f: number | undefined
const clear = () => {
if (f !== undefined) window.clearTimeout(f)
f = undefined
}
onCleanup(clear)
createEffect(
on(
working,
(on, prev) => {
clear()
if (on) {
setSlot({ open: true, show: true, fade: false })
return
}
if (prev) {
setSlot({ open: false, show: true, fade: true })
f = window.setTimeout(() => setSlot({ show: false, fade: false }), 260)
return
}
setSlot({ open: false, show: false, fade: false })
},
{ defer: true },
),
)
const activeMessageID = createMemo(() => { const activeMessageID = createMemo(() => {
const parentID = pending()?.parentID const parentID = pending()?.parentID
if (parentID) { if (parentID) {
@@ -573,43 +608,64 @@ export function MessageTimeline(props: {
aria-label={language.t("common.goBack")} aria-label={language.t("common.goBack")}
/> />
</Show> </Show>
<Show when={titleValue() || title.editing}> <div class="flex items-center min-w-0 grow-1">
<Show <div
when={title.editing} class="shrink-0 flex items-center justify-center overflow-hidden transition-[width,margin] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]"
fallback={ style={{
<h1 width: slot.open ? "16px" : "0px",
class="text-14-medium text-text-strong truncate grow-1 min-w-0 pl-2" "margin-right": slot.open ? "8px" : "0px",
onDblClick={openTitleEditor} }}
> aria-hidden="true"
{titleValue()}
</h1>
}
> >
<InlineInput <Show when={slot.show}>
ref={(el) => { <div
titleRef = el class="transition-opacity duration-200 ease-out"
}} classList={{
value={title.draft} "opacity-0": slot.fade,
disabled={title.saving} }}
class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]" >
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }} <Spinner class="size-4" style={{ color: "var(--icon-interactive-base)" }} />
onInput={(event) => setTitle("draft", event.currentTarget.value)} </div>
onKeyDown={(event) => { </Show>
event.stopPropagation() </div>
if (event.key === "Enter") { <Show when={titleValue() || title.editing}>
event.preventDefault() <Show
void saveTitleEditor() when={title.editing}
return fallback={
} <h1
if (event.key === "Escape") { class="text-14-medium text-text-strong truncate grow-1 min-w-0"
event.preventDefault() onDblClick={openTitleEditor}
closeTitleEditor() >
} {titleValue()}
}} </h1>
onBlur={closeTitleEditor} }
/> >
<InlineInput
ref={(el) => {
titleRef = el
}}
value={title.draft}
disabled={title.saving}
class="text-14-medium text-text-strong grow-1 min-w-0 rounded-[6px]"
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
onInput={(event) => setTitle("draft", event.currentTarget.value)}
onKeyDown={(event) => {
event.stopPropagation()
if (event.key === "Enter") {
event.preventDefault()
void saveTitleEditor()
return
}
if (event.key === "Escape") {
event.preventDefault()
closeTitleEditor()
}
}}
onBlur={closeTitleEditor}
/>
</Show>
</Show> </Show>
</Show> </div>
</div> </div>
<Show when={sessionID()}> <Show when={sessionID()}>
{(id) => ( {(id) => (
+1
View File
@@ -41,6 +41,7 @@ export function Spinner(props: {
animation: square.corner animation: square.corner
? undefined ? undefined
: `${square.outer ? "pulse-opacity-dim" : "pulse-opacity"} ${square.duration}s ease-in-out infinite`, : `${square.outer ? "pulse-opacity-dim" : "pulse-opacity"} ${square.duration}s ease-in-out infinite`,
"animation-fill-mode": square.corner ? undefined : "both",
"animation-delay": square.corner ? undefined : `${square.delay}s`, "animation-delay": square.corner ? undefined : `${square.delay}s`,
}} }}
/> />
+2 -2
View File
@@ -26,10 +26,10 @@
@keyframes pulse-opacity-dim { @keyframes pulse-opacity-dim {
0%, 0%,
100% { 100% {
opacity: 0; opacity: 0.15;
} }
50% { 50% {
opacity: 0.2; opacity: 0.35;
} }
} }