Skip to content

Commit 65b2a10

Browse files
authored
fade in prompt metadata transitions (#23037)
1 parent 7605acf commit 65b2a10

5 files changed

Lines changed: 87 additions & 22 deletions

File tree

packages/opencode/src/cli/cmd/tui/app.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,16 @@ export function tui(input: {
148148
<ExitProvider onBeforeExit={onBeforeExit} onExit={onExit}>
149149
<KVProvider>
150150
<ToastProvider>
151-
<RouteProvider>
151+
<RouteProvider
152+
initialRoute={
153+
(input.args.sessionID || input.args.continue) && !input.args.fork
154+
? {
155+
type: "session",
156+
sessionID: "dummy",
157+
}
158+
: undefined
159+
}
160+
>
152161
<TuiConfigProvider config={input.config}>
153162
<SDKProvider
154163
url={input.url}
@@ -333,13 +342,6 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
333342
})
334343
local.model.set({ providerID, modelID }, { recent: true })
335344
}
336-
// Handle --session without --fork immediately (fork is handled in createEffect below)
337-
if (args.sessionID && !args.fork) {
338-
route.navigate({
339-
type: "session",
340-
sessionID: args.sessionID,
341-
})
342-
}
343345
})
344346
})
345347

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1-
import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, decodePasteBytes } from "@opentui/core"
2-
import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js"
1+
import { BoxRenderable, RGBA, TextareaRenderable, MouseEvent, PasteEvent, decodePasteBytes } from "@opentui/core"
2+
import {
3+
createEffect,
4+
createMemo,
5+
onMount,
6+
createSignal,
7+
onCleanup,
8+
on,
9+
Show,
10+
Switch,
11+
Match,
12+
} from "solid-js"
313
import "opentui-spinner/solid"
414
import path from "path"
515
import { fileURLToPath } from "url"
@@ -35,6 +45,7 @@ import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
3545
import { DialogAlert } from "../../ui/dialog-alert"
3646
import { useToast } from "../../ui/toast"
3747
import { useKV } from "../../context/kv"
48+
import { createFadeIn } from "../../util/signal"
3849
import { useTextareaKeybindings } from "../textarea-keybindings"
3950
import { DialogSkill } from "../dialog-skill"
4051
import { useArgs } from "@tui/context/args"
@@ -75,6 +86,10 @@ function randomIndex(count: number) {
7586
return Math.floor(Math.random() * count)
7687
}
7788

89+
function fadeColor(color: RGBA, alpha: number) {
90+
return RGBA.fromValues(color.r, color.g, color.b, color.a * alpha)
91+
}
92+
7893
let stashed: { prompt: PromptInfo; cursor: number } | undefined
7994

8095
export function Prompt(props: PromptProps) {
@@ -97,6 +112,7 @@ export function Prompt(props: PromptProps) {
97112
const renderer = useRenderer()
98113
const { theme, syntax } = useTheme()
99114
const kv = useKV()
115+
const animationsEnabled = createMemo(() => kv.get("animations_enabled", true))
100116
const list = createMemo(() => props.placeholders?.normal ?? [])
101117
const shell = createMemo(() => props.placeholders?.shell ?? [])
102118
const [auto, setAuto] = createSignal<AutocompleteRef>()
@@ -858,6 +874,13 @@ export function Prompt(props: PromptProps) {
858874
return !!current
859875
})
860876

877+
const agentMetaAlpha = createFadeIn(() => !!local.agent.current(), animationsEnabled)
878+
const modelMetaAlpha = createFadeIn(() => !!local.agent.current() && store.mode === "normal", animationsEnabled)
879+
const variantMetaAlpha = createFadeIn(
880+
() => !!local.agent.current() && store.mode === "normal" && showVariant(),
881+
animationsEnabled,
882+
)
883+
861884
const placeholderText = createMemo(() => {
862885
if (props.showPlaceholder === false) return undefined
863886
if (store.mode === "shell") {
@@ -1133,17 +1156,24 @@ export function Prompt(props: PromptProps) {
11331156
<Show when={local.agent.current()} fallback={<box height={1} />}>
11341157
{(agent) => (
11351158
<>
1136-
<text fg={highlight()}>{store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)} </text>
1159+
<text fg={fadeColor(highlight(), agentMetaAlpha())}>
1160+
{store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)}{" "}
1161+
</text>
11371162
<Show when={store.mode === "normal"}>
11381163
<box flexDirection="row" gap={1}>
1139-
<text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
1164+
<text
1165+
flexShrink={0}
1166+
fg={fadeColor(keybind.leader ? theme.textMuted : theme.text, modelMetaAlpha())}
1167+
>
11401168
{local.model.parsed().model}
11411169
</text>
1142-
<text fg={theme.textMuted}>{currentProviderLabel()}</text>
1170+
<text fg={fadeColor(theme.textMuted, modelMetaAlpha())}>{currentProviderLabel()}</text>
11431171
<Show when={showVariant()}>
1144-
<text fg={theme.textMuted}>·</text>
1172+
<text fg={fadeColor(theme.textMuted, variantMetaAlpha())}>·</text>
11451173
<text>
1146-
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
1174+
<span style={{ fg: fadeColor(theme.warning, variantMetaAlpha()), bold: true }}>
1175+
{local.model.variant.current()}
1176+
</span>
11471177
</text>
11481178
</Show>
11491179
</box>

packages/opencode/src/cli/cmd/tui/context/route.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ export type Route = HomeRoute | SessionRoute | PluginRoute
2323

2424
export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
2525
name: "Route",
26-
init: () => {
26+
init: (props: { initialRoute?: Route }) => {
2727
const [store, setStore] = createStore<Route>(
28-
process.env["OPENCODE_ROUTE"]
29-
? JSON.parse(process.env["OPENCODE_ROUTE"])
30-
: {
31-
type: "home",
32-
},
28+
props.initialRoute ??
29+
(process.env["OPENCODE_ROUTE"]
30+
? JSON.parse(process.env["OPENCODE_ROUTE"])
31+
: {
32+
type: "home",
33+
}),
3334
)
3435

3536
return {

packages/opencode/src/cli/cmd/tui/context/sync.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
467467
return store.status
468468
},
469469
get ready() {
470+
return true
470471
if (process.env.OPENCODE_FAST_BOOT) return true
471472
return store.status !== "loading"
472473
},
Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,38 @@
1-
import { createSignal, type Accessor } from "solid-js"
1+
import { createEffect, createSignal, on, onCleanup, type Accessor } from "solid-js"
22
import { debounce, type Scheduled } from "@solid-primitives/scheduled"
33

44
export function createDebouncedSignal<T>(value: T, ms: number): [Accessor<T>, Scheduled<[value: T]>] {
55
const [get, set] = createSignal(value)
66
return [get, debounce((v: T) => set(() => v), ms)]
77
}
8+
9+
export function createFadeIn(show: Accessor<boolean>, enabled: Accessor<boolean>) {
10+
const [alpha, setAlpha] = createSignal(show() ? 1 : 0)
11+
12+
createEffect(
13+
on([show, enabled], ([visible, animate], previous) => {
14+
if (!visible) {
15+
setAlpha(0)
16+
return
17+
}
18+
19+
if (!animate || !previous) {
20+
setAlpha(1)
21+
return
22+
}
23+
24+
const start = performance.now()
25+
setAlpha(0)
26+
27+
const timer = setInterval(() => {
28+
const progress = Math.min((performance.now() - start) / 160, 1)
29+
setAlpha(progress * progress * (3 - 2 * progress))
30+
if (progress >= 1) clearInterval(timer)
31+
}, 16)
32+
33+
onCleanup(() => clearInterval(timer))
34+
}),
35+
)
36+
37+
return alpha
38+
}

0 commit comments

Comments
 (0)