2
0
Эх сурвалжийг харах

fade in prompt metadata transitions (#23037)

Dax 17 цаг өмнө
parent
commit
65b2a10e97

+ 10 - 8
packages/opencode/src/cli/cmd/tui/app.tsx

@@ -148,7 +148,16 @@ export function tui(input: {
             <ExitProvider onBeforeExit={onBeforeExit} onExit={onExit}>
               <KVProvider>
                 <ToastProvider>
-                  <RouteProvider>
+                  <RouteProvider
+                    initialRoute={
+                      (input.args.sessionID || input.args.continue) && !input.args.fork
+                        ? {
+                            type: "session",
+                            sessionID: "dummy",
+                          }
+                        : undefined
+                    }
+                  >
                     <TuiConfigProvider config={input.config}>
                       <SDKProvider
                         url={input.url}
@@ -333,13 +342,6 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
           })
         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,
-        })
-      }
     })
   })
 

+ 37 - 7
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

@@ -1,5 +1,15 @@
-import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, decodePasteBytes } from "@opentui/core"
-import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js"
+import { BoxRenderable, RGBA, TextareaRenderable, MouseEvent, PasteEvent, decodePasteBytes } from "@opentui/core"
+import {
+  createEffect,
+  createMemo,
+  onMount,
+  createSignal,
+  onCleanup,
+  on,
+  Show,
+  Switch,
+  Match,
+} from "solid-js"
 import "opentui-spinner/solid"
 import path from "path"
 import { fileURLToPath } from "url"
@@ -35,6 +45,7 @@ import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
 import { DialogAlert } from "../../ui/dialog-alert"
 import { useToast } from "../../ui/toast"
 import { useKV } from "../../context/kv"
+import { createFadeIn } from "../../util/signal"
 import { useTextareaKeybindings } from "../textarea-keybindings"
 import { DialogSkill } from "../dialog-skill"
 import { useArgs } from "@tui/context/args"
@@ -75,6 +86,10 @@ function randomIndex(count: number) {
   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
 
 export function Prompt(props: PromptProps) {
@@ -97,6 +112,7 @@ export function Prompt(props: PromptProps) {
   const renderer = useRenderer()
   const { theme, syntax } = useTheme()
   const kv = useKV()
+  const animationsEnabled = createMemo(() => kv.get("animations_enabled", true))
   const list = createMemo(() => props.placeholders?.normal ?? [])
   const shell = createMemo(() => props.placeholders?.shell ?? [])
   const [auto, setAuto] = createSignal<AutocompleteRef>()
@@ -858,6 +874,13 @@ export function Prompt(props: PromptProps) {
     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(() => {
     if (props.showPlaceholder === false) return undefined
     if (store.mode === "shell") {
@@ -1133,17 +1156,24 @@ export function Prompt(props: PromptProps) {
                 <Show when={local.agent.current()} fallback={<box height={1} />}>
                   {(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"}>
                         <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}
                           </text>
-                          <text fg={theme.textMuted}>{currentProviderLabel()}</text>
+                          <text fg={fadeColor(theme.textMuted, modelMetaAlpha())}>{currentProviderLabel()}</text>
                           <Show when={showVariant()}>
-                            <text fg={theme.textMuted}>·</text>
+                            <text fg={fadeColor(theme.textMuted, variantMetaAlpha())}>·</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>
                           </Show>
                         </box>

+ 7 - 6
packages/opencode/src/cli/cmd/tui/context/route.tsx

@@ -23,13 +23,14 @@ export type Route = HomeRoute | SessionRoute | PluginRoute
 
 export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
   name: "Route",
-  init: () => {
+  init: (props: { initialRoute?: Route }) => {
     const [store, setStore] = createStore<Route>(
-      process.env["OPENCODE_ROUTE"]
-        ? JSON.parse(process.env["OPENCODE_ROUTE"])
-        : {
-            type: "home",
-          },
+      props.initialRoute ??
+        (process.env["OPENCODE_ROUTE"]
+          ? JSON.parse(process.env["OPENCODE_ROUTE"])
+          : {
+              type: "home",
+            }),
     )
 
     return {

+ 1 - 0
packages/opencode/src/cli/cmd/tui/context/sync.tsx

@@ -467,6 +467,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
         return store.status
       },
       get ready() {
+        return true
         if (process.env.OPENCODE_FAST_BOOT) return true
         return store.status !== "loading"
       },

+ 32 - 1
packages/opencode/src/cli/cmd/tui/util/signal.ts

@@ -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"
 
 export function createDebouncedSignal<T>(value: T, ms: number): [Accessor<T>, Scheduled<[value: T]>] {
   const [get, set] = createSignal(value)
   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
+}