summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-10-31 09:45:57 -0500
committerAdam <[email protected]>2025-10-31 09:45:57 -0500
commit0ac943de909fb5a685a608d7894248cdd3a9e129 (patch)
tree6d2e0c0cb1a44de4f53e74a7002622842dab7591
parent485135cf5c4af64d1449aa3cadcdd0aef92201a3 (diff)
downloadopencode-0ac943de909fb5a685a608d7894248cdd3a9e129.tar.gz
opencode-0ac943de909fb5a685a608d7894248cdd3a9e129.zip
wip: desktop work
-rw-r--r--packages/desktop/src/pages/index.tsx15
-rw-r--r--packages/ui/src/components/index.ts1
-rw-r--r--packages/ui/src/components/typewriter.css14
-rw-r--r--packages/ui/src/components/typewriter.tsx54
-rw-r--r--packages/ui/src/styles/index.css1
5 files changed, 80 insertions, 5 deletions
diff --git a/packages/desktop/src/pages/index.tsx b/packages/desktop/src/pages/index.tsx
index 5237d78bb..01fe4b508 100644
--- a/packages/desktop/src/pages/index.tsx
+++ b/packages/desktop/src/pages/index.tsx
@@ -13,6 +13,7 @@ import {
DiffChanges,
ProgressCircle,
Message,
+ Typewriter,
} from "@opencode-ai/ui"
import { FileIcon } from "@/ui"
import FileTree from "@/components/file-tree"
@@ -544,7 +545,6 @@ export default function Page() {
<For each={local.session.userMessages()}>
{(message) => {
const diffs = createMemo(() => message.summary?.diffs ?? [])
-
return (
<li
class="group/li flex items-center gap-x-2 py-1 self-stretch cursor-default"
@@ -570,9 +570,9 @@ export default function Page() {
<div class="flex flex-col items-start gap-50 pb-50">
<For each={local.session.userMessages()}>
{(message) => {
+ const [initialized, setInitialized] = createSignal(!!message.summary?.title)
const [expanded, setExpanded] = createSignal(false)
const parts = createMemo(() => sync.data.part[message.id])
- const prompt = createMemo(() => local.session.getMessageText(message))
const title = createMemo(() => message.summary?.title)
const summary = createMemo(() => message.summary?.body)
const assistantMessages = createMemo(() => {
@@ -581,6 +581,9 @@ export default function Page() {
) as AssistantMessageType[]
})
const working = createMemo(() => !summary())
+ createEffect(() => {
+ setTimeout(() => setInitialized(!!title()), 10_000)
+ })
return (
<div
@@ -589,9 +592,11 @@ export default function Page() {
>
{/* Title */}
<div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10">
- <h1 class="text-14-medium text-text-strong overflow-hidden text-ellipsis min-w-0">
- {title() ?? prompt()}
- </h1>
+ <div class="text-14-medium text-text-strong overflow-hidden text-ellipsis min-w-0">
+ <Show when={initialized()} fallback={<Typewriter as="h1" text={title()} />}>
+ <h1>{title()}</h1>
+ </Show>
+ </div>
</div>
<Show when={title}>
<div class="-mt-8">
diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts
index 115e5f14f..499f2e27f 100644
--- a/packages/ui/src/components/index.ts
+++ b/packages/ui/src/components/index.ts
@@ -20,6 +20,7 @@ export * from "./select-dialog"
export * from "./tabs"
export * from "./basic-tool"
export * from "./tooltip"
+export * from "./typewriter"
export * from "../context/helper"
export * from "../context/shiki"
diff --git a/packages/ui/src/components/typewriter.css b/packages/ui/src/components/typewriter.css
new file mode 100644
index 000000000..e978312a9
--- /dev/null
+++ b/packages/ui/src/components/typewriter.css
@@ -0,0 +1,14 @@
+@keyframes blink {
+ 0%,
+ 50% {
+ opacity: 1;
+ }
+ 51%,
+ 100% {
+ opacity: 0;
+ }
+}
+
+.blinking-cursor {
+ animation: blink 1s step-end infinite;
+}
diff --git a/packages/ui/src/components/typewriter.tsx b/packages/ui/src/components/typewriter.tsx
new file mode 100644
index 000000000..9adb267ad
--- /dev/null
+++ b/packages/ui/src/components/typewriter.tsx
@@ -0,0 +1,54 @@
+import { createEffect, Show, type ValidComponent } from "solid-js"
+import { createStore } from "solid-js/store"
+import { Dynamic } from "solid-js/web"
+
+export const Typewriter = <T extends ValidComponent = "p">(props: {
+ text?: string
+ class?: string
+ as?: T
+}) => {
+ const [store, setStore] = createStore({
+ typing: false,
+ displayed: "",
+ cursor: true,
+ })
+
+ createEffect(() => {
+ const text = props.text
+ if (!text) return
+
+ let i = 0
+ setStore("typing", true)
+ setStore("displayed", "")
+ setStore("cursor", true)
+
+ const getTypingDelay = () => {
+ const random = Math.random()
+ if (random < 0.05) return 150 + Math.random() * 100
+ if (random < 0.15) return 80 + Math.random() * 60
+ return 30 + Math.random() * 50
+ }
+
+ const type = () => {
+ if (i < text.length) {
+ setStore("displayed", text.slice(0, i + 1))
+ i++
+ setTimeout(type, getTypingDelay())
+ } else {
+ setStore("typing", false)
+ setTimeout(() => setStore("cursor", false), 2000)
+ }
+ }
+
+ setTimeout(type, 200)
+ })
+
+ return (
+ <Dynamic component={props.as || "p"} class={props.class}>
+ {store.displayed}
+ <Show when={store.cursor}>
+ <span classList={{ "blinking-cursor": !store.typing }}>│</span>
+ </Show>
+ </Dynamic>
+ )
+}
diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css
index cea5a082d..146d957e2 100644
--- a/packages/ui/src/styles/index.css
+++ b/packages/ui/src/styles/index.css
@@ -25,5 +25,6 @@
@import "../components/select-dialog.css" layer(components);
@import "../components/tabs.css" layer(components);
@import "../components/tooltip.css" layer(components);
+@import "../components/typewriter.css" layer(components);
@import "./utilities.css" layer(utilities);