diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 7e883ec0e354..74eca9a0f2d4 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -148,7 +148,16 @@ export function tui(input: { - + Promise }) { }) 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, - }) - } }) }) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 82cdefebcbda..08540e62e416 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/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() @@ -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) { }> {(agent) => ( <> - {store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)} + + {store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)}{" "} + - + {local.model.parsed().model} - {currentProviderLabel()} + {currentProviderLabel()} - · + · - {local.model.variant.current()} + + {local.model.variant.current()} + diff --git a/packages/opencode/src/cli/cmd/tui/context/route.tsx b/packages/opencode/src/cli/cmd/tui/context/route.tsx index 6db824759238..35be17801b1f 100644 --- a/packages/opencode/src/cli/cmd/tui/context/route.tsx +++ b/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( - 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 { diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 57326e3a1aba..d2a7e5c4d085 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/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" }, diff --git a/packages/opencode/src/cli/cmd/tui/util/signal.ts b/packages/opencode/src/cli/cmd/tui/util/signal.ts index 15b57886d666..1c7cc0008d02 100644 --- a/packages/opencode/src/cli/cmd/tui/util/signal.ts +++ b/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(value: T, ms: number): [Accessor, Scheduled<[value: T]>] { const [get, set] = createSignal(value) return [get, debounce((v: T) => set(() => v), ms)] } + +export function createFadeIn(show: Accessor, enabled: Accessor) { + 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 +}