diff --git a/packages/desktop/src/components/message-progress.tsx b/packages/desktop/src/components/message-progress.tsx index fd66c5ca..5533ae41 100644 --- a/packages/desktop/src/components/message-progress.tsx +++ b/packages/desktop/src/components/message-progress.tsx @@ -1,7 +1,8 @@ -import { For, JSXElement, Match, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js" -import { Part } from "@opencode-ai/ui" +import { For, JSXElement, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js" +import { Markdown, Part } from "@opencode-ai/ui" import { useSync } from "@/context/sync" import type { AssistantMessage as AssistantMessageType, Part as PartType, ToolPart } from "@opencode-ai/sdk" +import { Spinner } from "./spinner" export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[]; done?: boolean }) { const sync = useSync() @@ -22,37 +23,42 @@ export function MessageProgress(props: { assistantMessages: () => AssistantMessa ) as ToolPart, ) - const eligibleItems = createMemo(() => { - let allParts = parts() + const resolvedParts = createMemo(() => { + let resolved = parts() const task = currentTask() if (task && task.state && "metadata" in task.state && task.state.metadata?.sessionId) { const messages = sync.data.message[task.state.metadata.sessionId as string]?.filter((m) => m.role === "assistant") - allParts = messages?.flatMap((m) => sync.data.part[m.id]) ?? parts() + resolved = messages?.flatMap((m) => sync.data.part[m.id]) ?? parts() } - return allParts.filter( - (p) => - p?.type === "text" || - (p?.type === "reasoning" && p.time?.end) || - (p?.type === "tool" && p.state.status === "completed"), - ) + return resolved + }) + const currentText = createMemo( + () => + resolvedParts().findLast((p) => p?.type === "text")?.text || + resolvedParts().findLast((p) => p?.type === "reasoning")?.text, + ) + const eligibleItems = createMemo(() => { + return resolvedParts().filter((p) => p?.type === "tool" && p.state.status === "completed") }) const finishedItems = createMemo<(JSXElement | PartType)[]>(() => [ - "", - "", -
Loading...
, +
, +
, +
+ Thinking... +
, ...eligibleItems(), - ...(done() ? ["", "", ""] : []), + ...(done() ? [
,
,
] : []), ]) - const MINIMUM_DELAY = 400 - const [visibleCount, setVisibleCount] = createSignal(1) + const delay = createMemo(() => (done() ? 220 : 400)) + const [visibleCount, setVisibleCount] = createSignal(eligibleItems().length) createEffect(() => { const total = finishedItems().length if (total > visibleCount()) { const timer = setTimeout(() => { setVisibleCount((prev) => prev + 1) - }, MINIMUM_DELAY) + }, delay()) onCleanup(() => clearTimeout(timer)) } else if (total < visibleCount()) { setVisibleCount(total) @@ -66,43 +72,57 @@ export function MessageProgress(props: { assistantMessages: () => AssistantMessa }) return ( -
+
- - {(part) => { - if (part && typeof part === "object" && "type" in part) { - const message = createMemo(() => sync.data.message[part.sessionID].find((m) => m.id === part.messageID)) - return ( -
- - - {(p) => ( -
- )} - - - {(p) => } - - {(p) => } - -
- ) - } - return
{part}
- }} - +
+ + {(part) => { + if (part && typeof part === "object" && "type" in part) { + const message = createMemo(() => sync.data.message[part.sessionID].find((m) => m.id === part.messageID)) + return ( +
+ + + {(p) => ( +
+ )} + + + {(p) => } + + + {(p) => } + + +
+ ) + } + return
{part}
+ }} + +
+ + {(text) => ( +
+ +
+ )} +
) } diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index 10487612..9b2c10df 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -334,7 +334,7 @@ export const PromptInput: Component = (props) => { onSubmit={handleSubmit} classList={{ "bg-surface-raised-stronger-non-alpha border border-border-strong-base": true, - "rounded-2xl overflow-clip focus-within:shadow-xs-border-selected": true, + "rounded-2xl overflow-clip focus-within:border-transparent focus-within:shadow-xs-border-select": true, [props.class ?? ""]: !!props.class, }} > diff --git a/packages/desktop/src/pages/index.tsx b/packages/desktop/src/pages/index.tsx index 929aeda7..35e415aa 100644 --- a/packages/desktop/src/pages/index.tsx +++ b/packages/desktop/src/pages/index.tsx @@ -548,7 +548,79 @@ export default function Page() { {(message) => { const diffs = createMemo(() => message.summary?.diffs ?? []) - const working = createMemo(() => !message.summary?.title) + const working = createMemo(() => !message.summary?.body) + const assistantMessages = createMemo(() => { + return sync.data.message[activeSession().id]?.filter( + (m) => m.role === "assistant" && m.parentID == message.id, + ) as AssistantMessageType[] + }) + const parts = createMemo(() => + assistantMessages().flatMap((m) => sync.data.part[m.id]), + ) + const lastPart = createMemo(() => parts().slice(-1)?.at(0)) + const rawStatus = createMemo(() => { + const defaultStatus = "Working..." + const last = lastPart() + if (!last) return defaultStatus + + if (last.type === "tool") { + switch (last.tool) { + case "task": + return "Delegating work..." + case "todowrite": + case "todoread": + return "Planning next steps..." + case "read": + return "Gathering context..." + case "list": + case "grep": + case "glob": + return "Searching the codebase..." + case "webfetch": + return "Searching the web..." + case "edit": + case "write": + return "Making edits..." + case "bash": + return "Running commands..." + default: + break + } + } else if (last.type === "reasoning") { + return "Thinking..." + } else if (last.type === "text") { + return "Gathering thoughts..." + } + return defaultStatus + }) + + const [status, setStatus] = createSignal(rawStatus()) + let lastStatusChange = Date.now() + let statusTimeout: number | undefined + + createEffect(() => { + const newStatus = rawStatus() + if (newStatus === status()) return + + const timeSinceLastChange = Date.now() - lastStatusChange + + if (timeSinceLastChange >= 1000) { + setStatus(newStatus) + lastStatusChange = Date.now() + if (statusTimeout) { + clearTimeout(statusTimeout) + statusTimeout = undefined + } + } else { + if (statusTimeout) clearTimeout(statusTimeout) + statusTimeout = setTimeout(() => { + setStatus(rawStatus()) + lastStatusChange = Date.now() + statusTimeout = undefined + }, 1000 - timeSinceLastChange) as unknown as number + } + }) + return (
  • @@ -604,10 +679,12 @@ export default function Page() { // allowing time for the animations to finish createEffect(() => { + title() setTimeout(() => setTitled(!!title()), 10_000) }) createEffect(() => { - setTimeout(() => setCompleted(!!summary()), 3_000) + summary() + setTimeout(() => setCompleted(!!summary()), 1200) }) return ( @@ -618,9 +695,18 @@ export default function Page() { > {/* Title */}
    -
    - }> -

    {title()}

    +
    + + } + > +

    {title()}

    @@ -628,7 +714,7 @@ export default function Page() {
    {/* Summary */} - +

    @@ -637,7 +723,9 @@ export default function Page() { Response

    - {(summary) => } + + {(summary) => } +
    @@ -699,8 +787,8 @@ export default function Page() {
    - Hide steps - Show steps + Hide details + Show details
    diff --git a/packages/ui/src/components/markdown.css b/packages/ui/src/components/markdown.css index 6af0f550..945c2764 100644 --- a/packages/ui/src/components/markdown.css +++ b/packages/ui/src/components/markdown.css @@ -6,12 +6,12 @@ color: var(--text-base); text-wrap: pretty; - /* text-14-regular */ + /* text-12-regular */ font-family: var(--font-family-sans); - font-size: var(--font-size-base); + font-size: var(--font-size-small); font-style: normal; font-weight: var(--font-weight-regular); - line-height: var(--line-height-large); /* 142.857% */ + line-height: var(--line-height-large); /* 166.667% */ letter-spacing: var(--letter-spacing-normal); &::-webkit-scrollbar { diff --git a/packages/ui/src/styles/animations.css b/packages/ui/src/styles/animations.css index ba93e65e..5fcebb93 100644 --- a/packages/ui/src/styles/animations.css +++ b/packages/ui/src/styles/animations.css @@ -11,3 +11,110 @@ opacity: 1; } } + +@keyframes fadeUp { + from { + opacity: 0; + transform: translateY(5px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.fade-up-text { + 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/styles/tailwind/index.css b/packages/ui/src/styles/tailwind/index.css index 658809df..d96240e7 100644 --- a/packages/ui/src/styles/tailwind/index.css +++ b/packages/ui/src/styles/tailwind/index.css @@ -63,7 +63,7 @@ --shadow-xs: var(--shadow-xs); --shadow-md: var(--shadow-md); - --shadow-xs-border-selected: var(--shadow-xs-border-selected); + --shadow-xs-border-select: var(--shadow-xs-border-select); --animate-pulse: var(--animate-pulse); } diff --git a/packages/ui/src/styles/tailwind/utilities.css b/packages/ui/src/styles/tailwind/utilities.css index fbb407b1..8194aeff 100644 --- a/packages/ui/src/styles/tailwind/utilities.css +++ b/packages/ui/src/styles/tailwind/utilities.css @@ -15,3 +15,99 @@ direction: rtl; text-align: left; } + +@utility fade-up-text { + 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; + } +}