2
0
Sebastian 3 долоо хоног өмнө
parent
commit
38af99dcb4

+ 35 - 5
.opencode/plugins/tui-smoke.tsx

@@ -1,5 +1,5 @@
 /** @jsxImportSource @opentui/solid */
-import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
+import { useKeyboard, useTerminalDimensions, type JSX } from "@opentui/solid"
 import { RGBA, VignetteEffect } from "@opentui/core"
 import type {
   TuiKeybindSet,
@@ -615,7 +615,7 @@ const Modal = (props: {
   )
 }
 
-const home = (input: Cfg): TuiSlotPlugin => ({
+const home = (api: TuiPluginApi, input: Cfg) => ({
   slots: {
     home_logo(ctx) {
       const map = ctx.theme.current
@@ -649,6 +649,36 @@ const home = (input: Cfg): TuiSlotPlugin => ({
         </box>
       )
     },
+    home_prompt(ctx, value) {
+      const skin = look(ctx.theme.current)
+      type Prompt = (props: {
+        workspaceID?: string
+        hint?: JSX.Element
+        placeholders?: {
+          normal?: string[]
+          shell?: string[]
+        }
+      }) => JSX.Element
+      if (!("Prompt" in api.ui)) return null
+      const view = api.ui.Prompt
+      if (typeof view !== "function") return null
+      const Prompt = view as Prompt
+      const normal = [
+        `[SMOKE] route check for ${input.label}`,
+        "[SMOKE] confirm home_prompt slot override",
+        "[SMOKE] verify api.ui.Prompt rendering",
+      ]
+      const shell = ["printf '[SMOKE] home prompt\n'", "git status --short", "bun --version"]
+      const Hint = (
+        <box flexShrink={0} flexDirection="row" gap={1}>
+          <text fg={skin.muted}>
+            <span style={{ fg: skin.accent }}>•</span> smoke home prompt
+          </text>
+        </box>
+      )
+
+      return <Prompt workspaceID={value.workspace_id} hint={Hint} placeholders={{ normal, shell }} />
+    },
     home_bottom(ctx) {
       const skin = look(ctx.theme.current)
       const text = "extra content in the unified home bottom slot"
@@ -706,8 +736,8 @@ const block = (input: Cfg, order: number, title: string, text: string): TuiSlotP
   },
 })
 
-const slot = (input: Cfg): TuiSlotPlugin[] => [
-  home(input),
+const slot = (api: TuiPluginApi, input: Cfg): TuiSlotPlugin[] => [
+  home(api, input),
   block(input, 50, "Smoke above", "renders above internal sidebar blocks"),
   block(input, 250, "Smoke between", "renders between internal sidebar blocks"),
   block(input, 650, "Smoke below", "renders below internal sidebar blocks"),
@@ -848,7 +878,7 @@ const tui: TuiPlugin = async (api, options, meta) => {
   ])
 
   reg(api, value, keys)
-  for (const item of slot(value)) {
+  for (const item of slot(api, value)) {
     api.slots.register(item)
   }
 }

+ 14 - 14
bun.lock

@@ -338,8 +338,8 @@
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/util": "workspace:*",
         "@openrouter/ai-sdk-provider": "2.3.3",
-        "@opentui/core": "0.1.91",
-        "@opentui/solid": "0.1.91",
+        "@opentui/core": "0.1.92",
+        "@opentui/solid": "0.1.92",
         "@parcel/watcher": "2.5.1",
         "@pierre/diffs": "catalog:",
         "@solid-primitives/event-bus": "1.1.2",
@@ -428,16 +428,16 @@
         "zod": "catalog:",
       },
       "devDependencies": {
-        "@opentui/core": "0.1.91",
-        "@opentui/solid": "0.1.91",
+        "@opentui/core": "0.1.92",
+        "@opentui/solid": "0.1.92",
         "@tsconfig/node22": "catalog:",
         "@types/node": "catalog:",
         "@typescript/native-preview": "catalog:",
         "typescript": "catalog:",
       },
       "peerDependencies": {
-        "@opentui/core": ">=0.1.91",
-        "@opentui/solid": ">=0.1.91",
+        "@opentui/core": ">=0.1.92",
+        "@opentui/solid": ">=0.1.92",
       },
       "optionalPeers": [
         "@opentui/core",
@@ -1459,21 +1459,21 @@
 
     "@opentelemetry/api": ["@opentelemetry/[email protected]", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
 
-    "@opentui/core": ["@opentui/[email protected]1", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.91", "@opentui/core-darwin-x64": "0.1.91", "@opentui/core-linux-arm64": "0.1.91", "@opentui/core-linux-x64": "0.1.91", "@opentui/core-win32-arm64": "0.1.91", "@opentui/core-win32-x64": "0.1.91", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-xkuBDChHix3lHESQZTWXnPi0c8aANtg0567te3Am2O9EB3V1afKYdOYRV7RrzC+VBNmkymD8dUN+jzLkEUnAEw=="],
+    "@opentui/core": ["@opentui/[email protected]2", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.92", "@opentui/core-darwin-x64": "0.1.92", "@opentui/core-linux-arm64": "0.1.92", "@opentui/core-linux-x64": "0.1.92", "@opentui/core-win32-arm64": "0.1.92", "@opentui/core-win32-x64": "0.1.92", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-c+KdYAIH3M8n24RYaor+t7AQtKZ3l84L7xdP7DEaN4xtuYH8W08E6Gi+wUal4g+HSai3HS9irox68yFf0VPAxw=="],
 
-    "@opentui/core-darwin-arm64": ["@opentui/[email protected]1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WlIMa832vyjHCJsteWtSDsTAOrOPw/LQjYXVPISwwKo5Puyyl9vWNsF+69eYEyFEh15u8JNNrOPK98nlXq8SOA=="],
+    "@opentui/core-darwin-arm64": ["@opentui/[email protected]2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NX/qFRuc7My0pazyOrw9fdTXmU7omXcZzQuHcsaVnwssljaT52UYMrJ7mCKhSo69RhHw0lnGCymTorvz3XBdsA=="],
 
-    "@opentui/core-darwin-x64": ["@opentui/[email protected]1", "", { "os": "darwin", "cpu": "x64" }, "sha512-nFZgQrdGtEzf5GXg4YxtDzxHvSwAig2G4Qf6ySN6sU9f9eaB1NJNhOVYLNJHBVEs5qOamBee+nXYEtG6zInIFQ=="],
+    "@opentui/core-darwin-x64": ["@opentui/[email protected]2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Zb4jn33hOf167llINKLniOabQIycs14LPOBZnQ6l4khbeeTPVJdG8gy9PhlAyIQygDKmRTFncVlP0RP+L6C7og=="],
 
-    "@opentui/core-linux-arm64": ["@opentui/[email protected]1", "", { "os": "linux", "cpu": "arm64" }, "sha512-vXAcHZaS3QzEXYyvM9KoE0juSOMPPPdNrV5Fo4HAbI5BXGCkMNQJoN0j0EzoO9xwfsO+EulRSHCLVTNkvI4n8Q=="],
+    "@opentui/core-linux-arm64": ["@opentui/[email protected]2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4VA1A91OTMPJ3LkAyaxKEZVJsk5jIc3Kz0gV2vip8p2aGLPpYHHpkFZpXP/FyzsnJzoSGftBeA6ya1GKa5bkXg=="],
 
-    "@opentui/core-linux-x64": ["@opentui/[email protected]1", "", { "os": "linux", "cpu": "x64" }, "sha512-rAJ9sOvvI9eoWHjVj6TLPDRqYPYISmfCm2TDxi67BO27+E7naJANHIIxMC7yhPAmwBof7plioL2lwl2UFXAoXw=="],
+    "@opentui/core-linux-x64": ["@opentui/[email protected]2", "", { "os": "linux", "cpu": "x64" }, "sha512-tr7va8hfKS1uY+TBmulQBoBlwijzJk56K/U/L9/tbHfW7oJctqxPVwEFHIh1HDcOQ3/UhMMWGvMfeG6cFiK8/A=="],
 
-    "@opentui/core-win32-arm64": ["@opentui/[email protected]1", "", { "os": "win32", "cpu": "arm64" }, "sha512-teLe7uHvPnD/lOwTwZp2lUFfeT27dk6ZSLWk8hrhsAJ/Y0MyoaCUHAsg3nZ/p+I3pie5aZUR1f0vrJfaZ8ukJw=="],
+    "@opentui/core-win32-arm64": ["@opentui/[email protected]2", "", { "os": "win32", "cpu": "arm64" }, "sha512-34YM3uPtDjzUVeSnJWIK2J8mxyduzV7f3mYc4Hub0glNpUdM1jjzF2HvvvnrKK5ElzTsIcno3c3lOYT8yvG1Zg=="],
 
-    "@opentui/core-win32-x64": ["@opentui/[email protected]1", "", { "os": "win32", "cpu": "x64" }, "sha512-Odx9S1NYp3I2jgy5aj5k3/wb3M+yChEK7k8UUxxFt4R37V1/um8n6Cxw4nfid6T2C45KDGJ/0BYe6lGugJlnSg=="],
+    "@opentui/core-win32-x64": ["@opentui/[email protected]2", "", { "os": "win32", "cpu": "x64" }, "sha512-uk442kA2Vn0mmJHHqk5sPM+Zai/AN9sgl7egekhoEOUx2VK3gxftKsVlx2YVpCHTvTE/S+vnD2WpQaJk2SNjww=="],
 
-    "@opentui/solid": ["@opentui/[email protected]1", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.91", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-phqiOcmTgNy7aG7s3P6zyatrBc1f6DkuLDJmGqy6R9QuoS4Mn9MKdNQe6Ick03xRAZuaS6ZdG3kueNxIlUMTCA=="],
+    "@opentui/solid": ["@opentui/[email protected]2", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.92", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-0Sx1+6zRpmMJ5oDEY0JS9b9+eGd/Q0fPndNllrQNnp7w2FCjpXmvHdBdq+pFI6kFp01MHq2ZOkUU5zX5/9YMSQ=="],
 
     "@oslojs/asn1": ["@oslojs/[email protected]", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
 

+ 2 - 2
packages/opencode/package.json

@@ -102,8 +102,8 @@
     "@opencode-ai/sdk": "workspace:*",
     "@opencode-ai/util": "workspace:*",
     "@openrouter/ai-sdk-provider": "2.3.3",
-    "@opentui/core": "0.1.91",
-    "@opentui/solid": "0.1.91",
+    "@opentui/core": "0.1.92",
+    "@opentui/solid": "0.1.92",
     "@parcel/watcher": "2.5.1",
     "@pierre/diffs": "catalog:",
     "@solid-primitives/event-bus": "1.1.2",

+ 20 - 9
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

@@ -45,6 +45,10 @@ export type PromptProps = {
   ref?: (ref: PromptRef) => void
   hint?: JSX.Element
   showPlaceholder?: boolean
+  placeholders?: {
+    normal?: string[]
+    shell?: string[]
+  }
 }
 
 export type PromptRef = {
@@ -57,13 +61,16 @@ export type PromptRef = {
   submit(): void
 }
 
-const PLACEHOLDERS = ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"]
-const SHELL_PLACEHOLDERS = ["ls -la", "git status", "pwd"]
 const money = new Intl.NumberFormat("en-US", {
   style: "currency",
   currency: "USD",
 })
 
+function randomIndex(count: number) {
+  if (count <= 0) return 0
+  return Math.floor(Math.random() * count)
+}
+
 export function Prompt(props: PromptProps) {
   let input: TextareaRenderable
   let anchor: BoxRenderable
@@ -83,6 +90,8 @@ export function Prompt(props: PromptProps) {
   const renderer = useRenderer()
   const { theme, syntax } = useTheme()
   const kv = useKV()
+  const list = createMemo(() => props.placeholders?.normal ?? [])
+  const shell = createMemo(() => props.placeholders?.shell ?? [])
 
   function promptModelWarning() {
     toast.show({
@@ -152,7 +161,7 @@ export function Prompt(props: PromptProps) {
     interrupt: number
     placeholder: number
   }>({
-    placeholder: Math.floor(Math.random() * PLACEHOLDERS.length),
+    placeholder: randomIndex(list().length),
     prompt: {
       input: "",
       parts: [],
@@ -166,7 +175,7 @@ export function Prompt(props: PromptProps) {
     on(
       () => props.sessionID,
       () => {
-        setStore("placeholder", Math.floor(Math.random() * PLACEHOLDERS.length))
+        setStore("placeholder", randomIndex(list().length))
       },
       { defer: true },
     ),
@@ -801,12 +810,14 @@ export function Prompt(props: PromptProps) {
   })
 
   const placeholderText = createMemo(() => {
-    if (props.sessionID) return undefined
+    if (props.showPlaceholder === false) return undefined
     if (store.mode === "shell") {
-      const example = SHELL_PLACEHOLDERS[store.placeholder % SHELL_PLACEHOLDERS.length]
+      if (!shell().length) return undefined
+      const example = shell()[store.placeholder % shell().length]
       return `Run a command... "${example}"`
     }
-    return `Ask anything... "${PLACEHOLDERS[store.placeholder % PLACEHOLDERS.length]}"`
+    if (!list().length) return undefined
+    return `Ask anything... "${list()[store.placeholder % list().length]}"`
   })
 
   const spinnerDef = createMemo(() => {
@@ -922,7 +933,7 @@ export function Prompt(props: PromptProps) {
                   }
                 }
                 if (e.name === "!" && input.visualCursor.offset === 0) {
-                  setStore("placeholder", Math.floor(Math.random() * SHELL_PLACEHOLDERS.length))
+                  setStore("placeholder", randomIndex(shell().length))
                   setStore("mode", "shell")
                   e.preventDefault()
                   return
@@ -1097,7 +1108,7 @@ export function Prompt(props: PromptProps) {
           />
         </box>
         <box flexDirection="row" justifyContent="space-between">
-          <Show when={status().type !== "idle"} fallback={<text />}>
+          <Show when={status().type !== "idle"} fallback={props.hint ?? <text />}>
             <box
               flexDirection="row"
               gap={1}

+ 14 - 0
packages/opencode/src/cli/cmd/tui/plugin/api.tsx

@@ -14,6 +14,7 @@ import { DialogAlert } from "../ui/dialog-alert"
 import { DialogConfirm } from "../ui/dialog-confirm"
 import { DialogPrompt } from "../ui/dialog-prompt"
 import { DialogSelect, type DialogSelectOption as SelectOption } from "../ui/dialog-select"
+import { Prompt } from "../component/prompt"
 import type { useToast } from "../ui/toast"
 import { Installation } from "@/installation"
 import { createOpencodeClient, type OpencodeClient } from "@opencode-ai/sdk/v2"
@@ -287,6 +288,19 @@ export function createTuiApi(input: Input): TuiHostPluginApi {
           />
         )
       },
+      Prompt(props) {
+        return (
+          <Prompt
+            workspaceID={props.workspaceID}
+            visible={props.visible}
+            disabled={props.disabled}
+            onSubmit={props.onSubmit}
+            hint={props.hint}
+            showPlaceholder={props.showPlaceholder}
+            placeholders={props.placeholders}
+          />
+        )
+      },
       toast(inputToast) {
         input.toast.show({
           title: inputToast.title,

+ 18 - 9
packages/opencode/src/cli/cmd/tui/routes/home.tsx

@@ -15,6 +15,10 @@ import { TuiPluginRuntime } from "../plugin"
 
 // TODO: what is the best way to do this?
 let once = false
+const placeholder = {
+  normal: ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"],
+  shell: ["ls -la", "git status", "pwd"],
+}
 
 export function Home() {
   const sync = useSync()
@@ -49,11 +53,12 @@ export function Home() {
     </box>
   )
 
-  let prompt: PromptRef
+  let prompt: PromptRef | undefined
   const args = useArgs()
   const local = useLocal()
   onMount(() => {
     if (once) return
+    if (!prompt) return
     if (route.initialPrompt) {
       prompt.set(route.initialPrompt)
       once = true
@@ -69,6 +74,7 @@ export function Home() {
       () => sync.ready && local.model.ready,
       (ready) => {
         if (!ready) return
+        if (!prompt) return
         if (!args.prompt) return
         if (prompt.current?.input !== args.prompt) return
         prompt.submit()
@@ -89,14 +95,17 @@ export function Home() {
         </box>
         <box height={1} minHeight={0} flexShrink={1} />
         <box width="100%" maxWidth={75} zIndex={1000} paddingTop={1} flexShrink={0}>
-          <Prompt
-            ref={(r) => {
-              prompt = r
-              promptRef.set(r)
-            }}
-            hint={Hint}
-            workspaceID={route.workspaceID}
-          />
+          <TuiPluginRuntime.Slot name="home_prompt" mode="replace" workspace_id={route.workspaceID}>
+            <Prompt
+              ref={(r) => {
+                prompt = r
+                promptRef.set(r)
+              }}
+              hint={Hint}
+              workspaceID={route.workspaceID}
+              placeholders={placeholder}
+            />
+          </TuiPluginRuntime.Slot>
         </box>
         <TuiPluginRuntime.Slot name="home_bottom" />
         <box flexGrow={1} minHeight={0} />

+ 1 - 0
packages/opencode/test/fixture/tui-plugin.ts

@@ -231,6 +231,7 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi {
       DialogConfirm: () => null,
       DialogPrompt: () => null,
       DialogSelect: () => null,
+      Prompt: () => null,
       toast: () => {},
       dialog: {
         replace: () => {

+ 4 - 4
packages/plugin/package.json

@@ -21,8 +21,8 @@
     "zod": "catalog:"
   },
   "peerDependencies": {
-    "@opentui/core": ">=0.1.91",
-    "@opentui/solid": ">=0.1.91"
+    "@opentui/core": ">=0.1.92",
+    "@opentui/solid": ">=0.1.92"
   },
   "peerDependenciesMeta": {
     "@opentui/core": {
@@ -33,8 +33,8 @@
     }
   },
   "devDependencies": {
-    "@opentui/core": "0.1.91",
-    "@opentui/solid": "0.1.91",
+    "@opentui/core": "0.1.92",
+    "@opentui/solid": "0.1.92",
     "@tsconfig/node22": "catalog:",
     "@types/node": "catalog:",
     "typescript": "catalog:",

+ 17 - 0
packages/plugin/src/tui.ts

@@ -135,6 +135,19 @@ export type TuiDialogSelectProps<Value = unknown> = {
   current?: Value
 }
 
+export type TuiPromptProps = {
+  workspaceID?: string
+  visible?: boolean
+  disabled?: boolean
+  onSubmit?: () => void
+  hint?: JSX.Element
+  showPlaceholder?: boolean
+  placeholders?: {
+    normal?: string[]
+    shell?: string[]
+  }
+}
+
 export type TuiToast = {
   variant?: "info" | "success" | "warning" | "error"
   title?: string
@@ -279,6 +292,9 @@ export type TuiSidebarFileItem = {
 export type TuiSlotMap = {
   app: {}
   home_logo: {}
+  home_prompt: {
+    workspace_id?: string
+  }
   home_bottom: {}
   sidebar_title: {
     session_id: string
@@ -386,6 +402,7 @@ export type TuiPluginApi = {
     DialogConfirm: (props: TuiDialogConfirmProps) => JSX.Element
     DialogPrompt: (props: TuiDialogPromptProps) => JSX.Element
     DialogSelect: <Value = unknown>(props: TuiDialogSelectProps<Value>) => JSX.Element
+    Prompt: (props: TuiPromptProps) => JSX.Element
     toast: (input: TuiToast) => void
     dialog: TuiDialogStack
   }