Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions packages/opencode/src/cli/cmd/tui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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,
})
}
})
})

Expand Down
44 changes: 37 additions & 7 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand All @@ -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>()
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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>
Expand Down
13 changes: 7 additions & 6 deletions packages/opencode/src/cli/cmd/tui/context/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/cli/cmd/tui/context/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
33 changes: 32 additions & 1 deletion packages/opencode/src/cli/cmd/tui/util/signal.ts
Original file line number Diff line number Diff line change
@@ -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
}
Loading