diff --git a/packages/desktop/src/components/message-progress.tsx b/packages/desktop/src/components/message-progress.tsx
new file mode 100644
index 00000000..f77e196b
--- /dev/null
+++ b/packages/desktop/src/components/message-progress.tsx
@@ -0,0 +1,82 @@
+import { For, Match, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
+import { Part } from "@opencode-ai/ui"
+import { useSync } from "@/context/sync"
+import type { AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
+
+export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[] }) {
+ const sync = useSync()
+ const items = createMemo(() => props.assistantMessages().flatMap((m) => sync.data.part[m.id]))
+
+ const finishedItems = createMemo(() => [
+ "",
+ "",
+ "Loading...",
+ ...items().filter(
+ (p) =>
+ p?.type === "text" ||
+ (p?.type === "reasoning" && p.time?.end) ||
+ (p?.type === "tool" && p.state.status === "completed"),
+ ),
+ "",
+ ])
+
+ const MINIMUM_DELAY = 400
+ const [visibleCount, setVisibleCount] = createSignal(1)
+
+ createEffect(() => {
+ const total = finishedItems().length
+ if (total > visibleCount()) {
+ const timer = setTimeout(() => {
+ setVisibleCount((prev) => prev + 1)
+ }, MINIMUM_DELAY)
+ onCleanup(() => clearTimeout(timer))
+ } else if (total < visibleCount()) {
+ setVisibleCount(total)
+ }
+ })
+
+ const translateY = createMemo(() => {
+ const total = visibleCount()
+ if (total < 2) return "0px"
+ return `-${(total - 2) * 40 - 8}px`
+ })
+
+ return (
+
+ )
+}
diff --git a/packages/desktop/src/components/spinner.tsx b/packages/desktop/src/components/spinner.tsx
new file mode 100644
index 00000000..5fc4cda6
--- /dev/null
+++ b/packages/desktop/src/components/spinner.tsx
@@ -0,0 +1,39 @@
+import { ComponentProps, For } from "solid-js"
+
+export function Spinner(props: { class?: string; classList?: ComponentProps<"div">["classList"] }) {
+ const squares = Array.from({ length: 16 }, (_, i) => ({
+ id: i,
+ x: (i % 4) * 4,
+ y: Math.floor(i / 4) * 4,
+ delay: Math.random() * 3,
+ duration: 2 + Math.random() * 2,
+ }))
+
+ return (
+
+ )
+}
diff --git a/packages/desktop/src/pages/index.tsx b/packages/desktop/src/pages/index.tsx
index 5b2f4ecc..3eea97b6 100644
--- a/packages/desktop/src/pages/index.tsx
+++ b/packages/desktop/src/pages/index.tsx
@@ -17,6 +17,7 @@ import {
} from "@opencode-ai/ui"
import { FileIcon } from "@/ui"
import FileTree from "@/components/file-tree"
+import { MessageProgress } from "@/components/message-progress"
import { For, onCleanup, onMount, Show, Match, Switch, createSignal, createEffect, createMemo } from "solid-js"
import { useLocal, type LocalFile } from "@/context/local"
import { createStore } from "solid-js/store"
@@ -39,6 +40,7 @@ import { useSync } from "@/context/sync"
import { useSDK } from "@/context/sdk"
import { type AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
import { Markdown } from "@opencode-ai/ui"
+import { Spinner } from "@/components/spinner"
export default function Page() {
const local = useLocal()
@@ -546,21 +548,31 @@ export default function Page() {
{(message) => {
const diffs = createMemo(() => message.summary?.diffs ?? [])
+ const working = createMemo(() => !message.summary?.title)
return (
- local.session.setActiveMessage(message.id)}
- >
-
-
+
+
+
+
+
+
+
+
+
+
+ {message.summary?.title ?? local.session.getMessageText(message)}
+
+
)
}}
@@ -600,19 +612,15 @@ export default function Page() {
-
-
-
-
-
+
+
+
{/* Summary */}
Summary
-
-
-
+ {(summary) => }
@@ -666,85 +674,7 @@ export default function Page() {
- {(_) => {
- const items = createMemo(() =>
- assistantMessages().flatMap((m) => sync.data.part[m.id]),
- )
- const finishedItems = createMemo(() =>
- items().filter(
- (p) =>
- (p?.type === "text" && p.time?.end) ||
- (p?.type === "reasoning" && p.time?.end) ||
- (p?.type === "tool" && p.state.status === "completed"),
- ),
- )
-
- const MINIMUM_DELAY = 800
- const [visibleCount, setVisibleCount] = createSignal(1)
-
- createEffect(() => {
- const total = finishedItems().length
- if (total > visibleCount()) {
- const timer = setTimeout(() => {
- setVisibleCount((prev) => prev + 1)
- }, MINIMUM_DELAY)
- onCleanup(() => clearTimeout(timer))
- } else if (total < visibleCount()) {
- setVisibleCount(total)
- }
- })
-
- const translateY = createMemo(() => {
- const total = visibleCount()
- if (total < 2) return "0px"
- return `-${(total - 2) * 48 - 8}px`
- })
-
- return (
-
- )
- }}
+
diff --git a/packages/ui/src/components/diff-changes.tsx b/packages/ui/src/components/diff-changes.tsx
index 433c47f3..e6c04f51 100644
--- a/packages/ui/src/components/diff-changes.tsx
+++ b/packages/ui/src/components/diff-changes.tsx
@@ -16,16 +16,6 @@ export function DiffChanges(props: { diff: FileDiff | FileDiff[]; variant?: "def
)
const total = createMemo(() => (additions() ?? 0) + (deletions() ?? 0))
- const countLines = (text: string) => {
- if (!text) return 0
- return text.split("\n").length
- }
-
- const totalBeforeLines = createMemo(() => {
- if (!Array.isArray(props.diff)) return countLines(props.diff.before || "")
- return props.diff.reduce((acc, diff) => acc + countLines(diff.before || ""), 0)
- })
-
const blockCounts = createMemo(() => {
const TOTAL_BLOCKS = 5
diff --git a/packages/ui/src/components/markdown.css b/packages/ui/src/components/markdown.css
index abc505a9..6af0f550 100644
--- a/packages/ui/src/components/markdown.css
+++ b/packages/ui/src/components/markdown.css
@@ -4,6 +4,7 @@
overflow: auto;
scrollbar-width: none;
color: var(--text-base);
+ text-wrap: pretty;
/* text-14-regular */
font-family: var(--font-family-sans);
@@ -34,4 +35,10 @@
margin-top: 16px;
margin-bottom: 16px;
}
+
+ hr {
+ margin-top: 8px;
+ margin-bottom: 16px;
+ border-color: var(--border-weaker-base);
+ }
}
diff --git a/packages/ui/src/styles/animations.css b/packages/ui/src/styles/animations.css
new file mode 100644
index 00000000..ba93e65e
--- /dev/null
+++ b/packages/ui/src/styles/animations.css
@@ -0,0 +1,13 @@
+:root {
+ --animate-pulse: pulse-opacity 2s ease-in-out infinite;
+}
+
+@keyframes pulse-opacity {
+ 0%,
+ 100% {
+ opacity: 0;
+ }
+ 50% {
+ opacity: 1;
+ }
+}
diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css
index 146d957e..e3cffc6c 100644
--- a/packages/ui/src/styles/index.css
+++ b/packages/ui/src/styles/index.css
@@ -28,3 +28,4 @@
@import "../components/typewriter.css" layer(components);
@import "./utilities.css" layer(utilities);
+@import "./animations.css" layer(utilities);
diff --git a/packages/ui/src/styles/tailwind/index.css b/packages/ui/src/styles/tailwind/index.css
index 76d8c7d3..658809df 100644
--- a/packages/ui/src/styles/tailwind/index.css
+++ b/packages/ui/src/styles/tailwind/index.css
@@ -64,6 +64,8 @@
--shadow-xs: var(--shadow-xs);
--shadow-md: var(--shadow-md);
--shadow-xs-border-selected: var(--shadow-xs-border-selected);
+
+ --animate-pulse: var(--animate-pulse);
}
@import "./colors.css";
diff --git a/packages/ui/src/styles/utilities.css b/packages/ui/src/styles/utilities.css
index 9c6b73f9..99b7760a 100644
--- a/packages/ui/src/styles/utilities.css
+++ b/packages/ui/src/styles/utilities.css
@@ -48,71 +48,6 @@
border-width: 0;
}
-.scroller {
- /* --fade-height: 1.5rem; */
- /**/
- /* --mask-top: linear-gradient(to bottom, transparent, black var(--fade-height)); */
- /* --mask-bottom: linear-gradient(to top, transparent, black var(--fade-height)); */
- /**/
- /* mask-image: var(--mask-top), var(--mask-bottom); */
- /* mask-repeat: no-repeat; */
- /* mask-size: 100% var(--fade-height); */
-
- animation: scroll-fade linear;
- animation-timeline: scroll(self);
-}
-
-/* Define the keyframes for the mask.
- These percentages now map to scroll positions:
- 0% = Scrolled to the top
- 100% = Scrolled to the bottom
-*/
-@keyframes scroll-fade {
- /* At the very top (0% scroll) */
- 0% {
- mask-image: linear-gradient(
- to bottom,
- black 90%,
- /* Opaque, but start fade to bottom */ transparent 100%
- );
- }
-
- /* A small amount scrolled (e.g., 5%)
- This is where the top fade should be fully visible.
- */
- 5% {
- mask-image: linear-gradient(
- to bottom,
- transparent 0%,
- black 10%,
- /* Fade-in top */ black 90%,
- /* Fade-out bottom */ transparent 100%
- );
- }
-
- /* Nearing the bottom (e.g., 95%)
- The bottom fade should start disappearing.
- */
- 95% {
- mask-image: linear-gradient(
- to bottom,
- transparent 0%,
- black 10%,
- /* Fade-in top */ black 90%,
- /* Fade-out bottom */ transparent 100%
- );
- }
-
- /* At the very bottom (100% scroll) */
- 100% {
- mask-image: linear-gradient(
- to bottom,
- transparent 0%,
- black 10% /* Opaque, but start fade from top */
- );
- }
-}
-
.truncate-start {
text-overflow: ellipsis;
overflow: hidden;