diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index ce6a01378..4060e5e5c 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -8,6 +8,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Dialog } from "@opencode-ai/ui/dialog" import { InlineInput } from "@opencode-ai/ui/inline-input" +import { Spinner } from "@opencode-ai/ui/spinner" import { SessionTurn } from "@opencode-ai/ui/session-turn" import { ScrollView } from "@opencode-ai/ui/scroll-view" 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 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 parentID = pending()?.parentID if (parentID) { @@ -573,43 +608,64 @@ export function MessageTimeline(props: { aria-label={language.t("common.goBack")} /> - - - {titleValue()} - - } +
+ + + + {titleValue()} + + } + > + { + 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} + /> + - +
{(id) => ( diff --git a/packages/ui/src/components/spinner.tsx b/packages/ui/src/components/spinner.tsx index cc4149d17..3d029d976 100644 --- a/packages/ui/src/components/spinner.tsx +++ b/packages/ui/src/components/spinner.tsx @@ -41,6 +41,7 @@ export function Spinner(props: { animation: square.corner ? undefined : `${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`, }} /> diff --git a/packages/ui/src/styles/animations.css b/packages/ui/src/styles/animations.css index f8d11e0e5..f9a09df37 100644 --- a/packages/ui/src/styles/animations.css +++ b/packages/ui/src/styles/animations.css @@ -26,10 +26,10 @@ @keyframes pulse-opacity-dim { 0%, 100% { - opacity: 0; + opacity: 0.15; } 50% { - opacity: 0.2; + opacity: 0.35; } }