fade in prompt metadata transitions (#23037)
This commit is contained in:
@@ -148,7 +148,16 @@ export function tui(input: {
|
|||||||
<ExitProvider onBeforeExit={onBeforeExit} onExit={onExit}>
|
<ExitProvider onBeforeExit={onBeforeExit} onExit={onExit}>
|
||||||
<KVProvider>
|
<KVProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<RouteProvider>
|
<RouteProvider
|
||||||
|
initialRoute={
|
||||||
|
(input.args.sessionID || input.args.continue) && !input.args.fork
|
||||||
|
? {
|
||||||
|
type: "session",
|
||||||
|
sessionID: "dummy",
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
<TuiConfigProvider config={input.config}>
|
<TuiConfigProvider config={input.config}>
|
||||||
<SDKProvider
|
<SDKProvider
|
||||||
url={input.url}
|
url={input.url}
|
||||||
@@ -333,13 +342,6 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
|||||||
})
|
})
|
||||||
local.model.set({ providerID, modelID }, { recent: true })
|
local.model.set({ providerID, modelID }, { recent: true })
|
||||||
}
|
}
|
||||||
// Handle --session without --fork immediately (fork is handled in createEffect below)
|
|
||||||
if (args.sessionID && !args.fork) {
|
|
||||||
route.navigate({
|
|
||||||
type: "session",
|
|
||||||
sessionID: args.sessionID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, decodePasteBytes } from "@opentui/core"
|
import { BoxRenderable, RGBA, TextareaRenderable, MouseEvent, PasteEvent, decodePasteBytes } from "@opentui/core"
|
||||||
import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js"
|
import {
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
onMount,
|
||||||
|
createSignal,
|
||||||
|
onCleanup,
|
||||||
|
on,
|
||||||
|
Show,
|
||||||
|
Switch,
|
||||||
|
Match,
|
||||||
|
} from "solid-js"
|
||||||
import "opentui-spinner/solid"
|
import "opentui-spinner/solid"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { fileURLToPath } from "url"
|
import { fileURLToPath } from "url"
|
||||||
@@ -35,6 +45,7 @@ import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
|
|||||||
import { DialogAlert } from "../../ui/dialog-alert"
|
import { DialogAlert } from "../../ui/dialog-alert"
|
||||||
import { useToast } from "../../ui/toast"
|
import { useToast } from "../../ui/toast"
|
||||||
import { useKV } from "../../context/kv"
|
import { useKV } from "../../context/kv"
|
||||||
|
import { createFadeIn } from "../../util/signal"
|
||||||
import { useTextareaKeybindings } from "../textarea-keybindings"
|
import { useTextareaKeybindings } from "../textarea-keybindings"
|
||||||
import { DialogSkill } from "../dialog-skill"
|
import { DialogSkill } from "../dialog-skill"
|
||||||
import { useArgs } from "@tui/context/args"
|
import { useArgs } from "@tui/context/args"
|
||||||
@@ -75,6 +86,10 @@ function randomIndex(count: number) {
|
|||||||
return Math.floor(Math.random() * count)
|
return Math.floor(Math.random() * count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fadeColor(color: RGBA, alpha: number) {
|
||||||
|
return RGBA.fromValues(color.r, color.g, color.b, color.a * alpha)
|
||||||
|
}
|
||||||
|
|
||||||
let stashed: { prompt: PromptInfo; cursor: number } | undefined
|
let stashed: { prompt: PromptInfo; cursor: number } | undefined
|
||||||
|
|
||||||
export function Prompt(props: PromptProps) {
|
export function Prompt(props: PromptProps) {
|
||||||
@@ -97,6 +112,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
const { theme, syntax } = useTheme()
|
const { theme, syntax } = useTheme()
|
||||||
const kv = useKV()
|
const kv = useKV()
|
||||||
|
const animationsEnabled = createMemo(() => kv.get("animations_enabled", true))
|
||||||
const list = createMemo(() => props.placeholders?.normal ?? [])
|
const list = createMemo(() => props.placeholders?.normal ?? [])
|
||||||
const shell = createMemo(() => props.placeholders?.shell ?? [])
|
const shell = createMemo(() => props.placeholders?.shell ?? [])
|
||||||
const [auto, setAuto] = createSignal<AutocompleteRef>()
|
const [auto, setAuto] = createSignal<AutocompleteRef>()
|
||||||
@@ -858,6 +874,13 @@ export function Prompt(props: PromptProps) {
|
|||||||
return !!current
|
return !!current
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const agentMetaAlpha = createFadeIn(() => !!local.agent.current(), animationsEnabled)
|
||||||
|
const modelMetaAlpha = createFadeIn(() => !!local.agent.current() && store.mode === "normal", animationsEnabled)
|
||||||
|
const variantMetaAlpha = createFadeIn(
|
||||||
|
() => !!local.agent.current() && store.mode === "normal" && showVariant(),
|
||||||
|
animationsEnabled,
|
||||||
|
)
|
||||||
|
|
||||||
const placeholderText = createMemo(() => {
|
const placeholderText = createMemo(() => {
|
||||||
if (props.showPlaceholder === false) return undefined
|
if (props.showPlaceholder === false) return undefined
|
||||||
if (store.mode === "shell") {
|
if (store.mode === "shell") {
|
||||||
@@ -1133,17 +1156,24 @@ export function Prompt(props: PromptProps) {
|
|||||||
<Show when={local.agent.current()} fallback={<box height={1} />}>
|
<Show when={local.agent.current()} fallback={<box height={1} />}>
|
||||||
{(agent) => (
|
{(agent) => (
|
||||||
<>
|
<>
|
||||||
<text fg={highlight()}>{store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)} </text>
|
<text fg={fadeColor(highlight(), agentMetaAlpha())}>
|
||||||
|
{store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)}{" "}
|
||||||
|
</text>
|
||||||
<Show when={store.mode === "normal"}>
|
<Show when={store.mode === "normal"}>
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
<text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
|
<text
|
||||||
|
flexShrink={0}
|
||||||
|
fg={fadeColor(keybind.leader ? theme.textMuted : theme.text, modelMetaAlpha())}
|
||||||
|
>
|
||||||
{local.model.parsed().model}
|
{local.model.parsed().model}
|
||||||
</text>
|
</text>
|
||||||
<text fg={theme.textMuted}>{currentProviderLabel()}</text>
|
<text fg={fadeColor(theme.textMuted, modelMetaAlpha())}>{currentProviderLabel()}</text>
|
||||||
<Show when={showVariant()}>
|
<Show when={showVariant()}>
|
||||||
<text fg={theme.textMuted}>·</text>
|
<text fg={fadeColor(theme.textMuted, variantMetaAlpha())}>·</text>
|
||||||
<text>
|
<text>
|
||||||
<span style={{ fg: theme.warning, bold: true }}>{local.model.variant.current()}</span>
|
<span style={{ fg: fadeColor(theme.warning, variantMetaAlpha()), bold: true }}>
|
||||||
|
{local.model.variant.current()}
|
||||||
|
</span>
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
|
|||||||
@@ -23,13 +23,14 @@ export type Route = HomeRoute | SessionRoute | PluginRoute
|
|||||||
|
|
||||||
export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
|
export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
|
||||||
name: "Route",
|
name: "Route",
|
||||||
init: () => {
|
init: (props: { initialRoute?: Route }) => {
|
||||||
const [store, setStore] = createStore<Route>(
|
const [store, setStore] = createStore<Route>(
|
||||||
process.env["OPENCODE_ROUTE"]
|
props.initialRoute ??
|
||||||
? JSON.parse(process.env["OPENCODE_ROUTE"])
|
(process.env["OPENCODE_ROUTE"]
|
||||||
: {
|
? JSON.parse(process.env["OPENCODE_ROUTE"])
|
||||||
type: "home",
|
: {
|
||||||
},
|
type: "home",
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -467,6 +467,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
return store.status
|
return store.status
|
||||||
},
|
},
|
||||||
get ready() {
|
get ready() {
|
||||||
|
return true
|
||||||
if (process.env.OPENCODE_FAST_BOOT) return true
|
if (process.env.OPENCODE_FAST_BOOT) return true
|
||||||
return store.status !== "loading"
|
return store.status !== "loading"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,38 @@
|
|||||||
import { createSignal, type Accessor } from "solid-js"
|
import { createEffect, createSignal, on, onCleanup, type Accessor } from "solid-js"
|
||||||
import { debounce, type Scheduled } from "@solid-primitives/scheduled"
|
import { debounce, type Scheduled } from "@solid-primitives/scheduled"
|
||||||
|
|
||||||
export function createDebouncedSignal<T>(value: T, ms: number): [Accessor<T>, Scheduled<[value: T]>] {
|
export function createDebouncedSignal<T>(value: T, ms: number): [Accessor<T>, Scheduled<[value: T]>] {
|
||||||
const [get, set] = createSignal(value)
|
const [get, set] = createSignal(value)
|
||||||
return [get, debounce((v: T) => set(() => v), ms)]
|
return [get, debounce((v: T) => set(() => v), ms)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createFadeIn(show: Accessor<boolean>, enabled: Accessor<boolean>) {
|
||||||
|
const [alpha, setAlpha] = createSignal(show() ? 1 : 0)
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on([show, enabled], ([visible, animate], previous) => {
|
||||||
|
if (!visible) {
|
||||||
|
setAlpha(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!animate || !previous) {
|
||||||
|
setAlpha(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now()
|
||||||
|
setAlpha(0)
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
const progress = Math.min((performance.now() - start) / 160, 1)
|
||||||
|
setAlpha(progress * progress * (3 - 2 * progress))
|
||||||
|
if (progress >= 1) clearInterval(timer)
|
||||||
|
}, 16)
|
||||||
|
|
||||||
|
onCleanup(() => clearInterval(timer))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return alpha
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user