Răsfoiți Sursa

fix(tui): improve narrow layouts (#39918)

Kit Langton 2 săptămâni în urmă
părinte
comite
0481dba88e

+ 26 - 9
packages/tui/src/component/logo.tsx

@@ -1,11 +1,13 @@
 import { RGBA, TextAttributes } from "@opentui/core"
 import { For, type JSX } from "solid-js"
+import { useTerminalDimensions } from "@opentui/solid"
 import { useTheme } from "../context/theme"
 import { tint } from "../theme/color"
-import { logo } from "../logo"
+import { go, logo } from "../logo"
 
 export function Logo() {
   const theme = useTheme()
+  const dimensions = useTerminalDimensions()
 
   const renderLine = (line: string, fg: RGBA, bold: boolean): JSX.Element[] => {
     const shadow = tint(theme.background.default, fg, 0.25)
@@ -49,14 +51,29 @@ export function Logo() {
 
   return (
     <box>
-      <For each={logo.left}>
-        {(line, index) => (
-          <box flexDirection="row" gap={1}>
-            <box flexDirection="row">{renderLine(line, theme.text.subdued, false)}</box>
-            <box flexDirection="row">{renderLine(logo.right[index()], theme.text.default, true)}</box>
-          </box>
-        )}
-      </For>
+      {dimensions().height < 12 ? null : dimensions().width < 22 ? (
+        <For each={go.right.slice(1)}>
+          {(line) => <box flexDirection="row">{renderLine(line, theme.text.default, true)}</box>}
+        </For>
+      ) : dimensions().width < 44 ? (
+        <>
+          <For each={logo.left.slice(1)}>
+            {(line) => <box flexDirection="row">{renderLine(line, theme.text.subdued, false)}</box>}
+          </For>
+          <For each={logo.right}>
+            {(line) => <box flexDirection="row">{renderLine(line, theme.text.default, true)}</box>}
+          </For>
+        </>
+      ) : (
+        <For each={logo.left}>
+          {(line, index) => (
+            <box flexDirection="row" gap={1}>
+              <box flexDirection="row">{renderLine(line, theme.text.subdued, false)}</box>
+              <box flexDirection="row">{renderLine(logo.right[index()], theme.text.default, true)}</box>
+            </box>
+          )}
+        </For>
+      )}
     </box>
   )
 }

+ 32 - 16
packages/tui/src/component/prompt/index.tsx

@@ -1314,13 +1314,20 @@ export function Prompt(props: PromptProps) {
 
   const placeholderText = createMemo(() => {
     if (props.showPlaceholder === false) return undefined
-    if (store.mode === "shell") {
-      if (!shell().length) return undefined
-      const example = shell()[store.placeholder % shell().length]
-      return `Run a command... "${example}"`
-    }
-    if (!list().length) return undefined
-    return `Ask anything... "${list()[store.placeholder % list().length]}"`
+    const value = (() => {
+      if (store.mode === "shell") {
+        if (!shell().length) return undefined
+        return `Run a command... "${shell()[store.placeholder % shell().length]}"`
+      }
+      if (!list().length) return undefined
+      return `Ask anything... "${list()[store.placeholder % list().length]}"`
+    })()
+    if (!value) return undefined
+    const width =
+      dimensions().width < 44
+        ? dimensions().width - 5
+        : Math.min(75, dimensions().width - 4) - 5
+    return Locale.takeWidth(value, Math.max(1, width)).trimEnd()
   })
   const locationLabel = createMemo(() => {
     if (!props.sessionID) {
@@ -1370,8 +1377,8 @@ export function Prompt(props: PromptProps) {
           }}
         >
           <box
-            paddingLeft={2}
-            paddingRight={2}
+            paddingLeft={dimensions().width < 44 ? 1 : 2}
+            paddingRight={dimensions().width < 44 ? 1 : 2}
             paddingTop={1}
             flexShrink={0}
             backgroundColor={promptBg()}
@@ -1461,25 +1468,34 @@ export function Prompt(props: PromptProps) {
               syntaxStyle={syntax()}
             />
             <box flexDirection="row" flexShrink={0} paddingTop={1} gap={1} justifyContent="space-between">
-              <box flexDirection="row" gap={1}>
+              <box flexDirection="row" gap={1} flexGrow={1} flexShrink={1} minWidth={0}>
                 <Show when={agentLabel()} fallback={<box height={1} />}>
                   {(label) => (
                     <>
                       <text fg={fadeColor(highlight(), agentMetaAlpha())}>{label()}</text>
-                      <Show when={store.mode === "normal" && local.permission.mode === "auto"}>
+                      <Show
+                        when={store.mode === "normal" && local.permission.mode === "auto" && dimensions().width >= 44}
+                      >
                         <text fg={fadeColor(theme.text.subdued, agentMetaAlpha())}>auto</text>
                       </Show>
-                      <Show when={store.mode === "normal"}>
-                        <box flexDirection="row" gap={1}>
+                      <Show when={store.mode === "normal" && dimensions().width >= 28}>
+                        <box flexDirection="row" gap={1} flexGrow={1} flexShrink={1} minWidth={0}>
                           <text fg={fadeColor(theme.text.subdued, modelMetaAlpha())}>·</text>
                           <text
-                            flexShrink={0}
+                            flexShrink={1}
+                            minWidth={0}
+                            wrapMode="none"
+                            truncate
                             fg={fadeColor(leader() ? theme.text.subdued : theme.text.default, modelMetaAlpha())}
                           >
                             {local.model.parsed().model}
                           </text>
-                          <text fg={fadeColor(theme.text.subdued, modelMetaAlpha())}>{currentProviderLabel()}</text>
-                          <Show when={showVariant()}>
+                          <Show when={dimensions().width >= 50}>
+                            <text flexShrink={0} fg={fadeColor(theme.text.subdued, modelMetaAlpha())}>
+                              {currentProviderLabel()}
+                            </text>
+                          </Show>
+                          <Show when={showVariant() && dimensions().width >= 70}>
                             <text fg={fadeColor(theme.text.subdued, variantMetaAlpha())}>·</text>
                             <text>
                               <span

+ 17 - 42
packages/tui/src/feature-plugins/home/footer.tsx

@@ -1,23 +1,6 @@
 import { Plugin } from "@opencode-ai/plugin/tui"
 import { createMemo, Match, Show, Switch } from "solid-js"
 import { useTerminalDimensions } from "@opentui/solid"
-import { stringWidth } from "../../util/string-width"
-import { FadeFilePath } from "../../ui/fade-file-path"
-
-function Directory(props: { context: Plugin.Context; maxWidth: number }) {
-  const directory = createMemo(() =>
-    props.context.location ? props.context.ui.format.path(props.context.location.directory) : undefined,
-  )
-
-  return (
-    <FadeFilePath
-      value={directory()}
-      maxWidth={props.maxWidth}
-      fg={props.context.theme.text.subdued}
-      bg={props.context.theme.background.default}
-    />
-  )
-}
 
 function Mcp(props: { context: Plugin.Context }) {
   const list = createMemo(() => props.context.data.location.mcp.server.list(props.context.location) ?? [])
@@ -53,34 +36,26 @@ function Mcp(props: { context: Plugin.Context }) {
 
 function View(props: { context: Plugin.Context }) {
   const dimensions = useTerminalDimensions()
-  const mcpWidth = createMemo(() => {
-    const list = props.context.data.location.mcp.server.list(props.context.location) ?? []
-    if (list.length === 0) return 0
-    const count = list.filter((item) => item.status.status === "connected").length
-    return stringWidth(`⊙ ${count} MCP /status`) + 2
-  })
 
   return (
-    <box
-      width="100%"
-      paddingTop={1}
-      paddingBottom={1}
-      paddingLeft={2}
-      paddingRight={2}
-      flexDirection="row"
-      flexShrink={0}
-      gap={2}
-    >
-      <Directory
-        context={props.context}
-        maxWidth={Math.max(2, dimensions().width - 8 - stringWidth(props.context.app.version) - mcpWidth())}
-      />
-      <Mcp context={props.context} />
-      <box flexGrow={1} />
-      <box flexShrink={0}>
-        <text fg={props.context.theme.text.subdued}>{props.context.app.version}</text>
+    <Show when={dimensions().height >= 12 && dimensions().width >= 44}>
+      <box
+        width="100%"
+        paddingTop={dimensions().height < 16 ? 0 : 1}
+        paddingBottom={dimensions().height < 16 ? 0 : 1}
+        paddingLeft={2}
+        paddingRight={2}
+        flexDirection="row"
+        flexShrink={0}
+        gap={2}
+      >
+        <Mcp context={props.context} />
+        <box flexGrow={1} />
+        <box flexShrink={0}>
+          <text fg={props.context.theme.text.subdued}>{props.context.app.version}</text>
+        </box>
       </box>
-    </box>
+    </Show>
   )
 }
 

+ 12 - 5
packages/tui/src/feature-plugins/prompt/footer.tsx

@@ -1,6 +1,7 @@
 import { Plugin } from "@opencode-ai/plugin/tui"
 import { createMemo, Match, Show, Switch } from "solid-js"
 import { contextUsage, formatContextUsage } from "../../util/session"
+import { useTerminalDimensions } from "@opentui/solid"
 
 const money = new Intl.NumberFormat("en-US", {
   style: "currency",
@@ -8,6 +9,7 @@ const money = new Intl.NumberFormat("en-US", {
 })
 
 export function PromptFooter(props: { context: Plugin.Context; sessionID?: string; mode: "normal" | "shell" }) {
+  const dimensions = useTerminalDimensions()
   const activeSubagents = createMemo(() => {
     if (!props.sessionID) return 0
     return props.context.data.session
@@ -60,19 +62,24 @@ export function PromptFooter(props: { context: Plugin.Context; sessionID?: strin
               <Show when={status().length > 0}>{status().join(" · ")}</Show>
             </text>
           </Match>
-          <Match when={true}>
+          <Match when={dimensions().width >= 44}>
             <text fg={props.context.theme.text.default} flexShrink={0}>
               {shortcut("agent.cycle")} <span style={{ fg: props.context.theme.text.subdued }}>agents</span>
             </text>
           </Match>
         </Switch>
-        <text fg={props.context.theme.text.default} flexShrink={0}>
-          {shortcut("command.palette.show")} <span style={{ fg: props.context.theme.text.subdued }}>commands</span>
-        </text>
+        <Show when={dimensions().width >= 44}>
+          <text fg={props.context.theme.text.default} flexShrink={0}>
+            {shortcut("command.palette.show")} <span style={{ fg: props.context.theme.text.subdued }}>commands</span>
+          </text>
+        </Show>
       </Match>
       <Match when={props.mode === "shell"}>
         <text fg={props.context.theme.text.default} flexShrink={0}>
-          esc <span style={{ fg: props.context.theme.text.subdued }}>exit shell mode</span>
+          esc{" "}
+          <span style={{ fg: props.context.theme.text.subdued }}>
+            {dimensions().width < 44 ? "shell" : "exit shell mode"}
+          </span>
         </text>
       </Match>
     </Switch>

+ 8 - 1
packages/tui/src/routes/home.tsx

@@ -10,6 +10,7 @@ import { useData } from "../context/data"
 import { useLocation } from "../context/location"
 import { FormPrompt } from "./session/form"
 import { PluginSlot } from "../plugin/context"
+import { useTerminalDimensions } from "@opentui/solid"
 
 let once = false
 const placeholder = {
@@ -26,6 +27,7 @@ export function Home() {
   const editor = useEditorContext()
   const data = useData()
   const location = useLocation()
+  const dimensions = useTerminalDimensions()
   // Global MCP elicitations can arrive without a session route, so keep them reachable from Home.
   const currentLocation = () => route.location ?? data.location.default()
   const forms = createMemo(() => data.session.form.list("global", currentLocation()) ?? [])
@@ -71,7 +73,12 @@ export function Home() {
 
   return (
     <>
-      <box flexGrow={1} alignItems="center" paddingLeft={2} paddingRight={2}>
+      <box
+        flexGrow={1}
+        alignItems="center"
+        paddingLeft={dimensions().width < 44 ? 1 : 2}
+        paddingRight={dimensions().width < 44 ? 1 : 2}
+      >
         <box flexGrow={1} minHeight={0} />
         <box height={4} minHeight={0} flexShrink={1} />
         <box flexShrink={0}>

+ 13 - 3
packages/tui/src/routes/session/index.tsx

@@ -958,7 +958,14 @@ export function Session() {
       }}
     >
       <box flexDirection="row" flexGrow={1} minHeight={0}>
-        <box flexGrow={1} minHeight={0} paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1}>
+        <box
+          flexGrow={1}
+          minHeight={0}
+          paddingBottom={1}
+          paddingLeft={dimensions().width < 44 ? 1 : 2}
+          paddingRight={dimensions().width < 44 ? 1 : 2}
+          gap={1}
+        >
           <Show when={session()}>
             <scrollbox
               ref={(r) => (scroll = r)}
@@ -1540,6 +1547,7 @@ function SessionGroupView(props: {
 function AssistantFooter(props: { message: SessionMessageAssistant }) {
   const ctx = use()
   const local = useLocal()
+  const dimensions = useTerminalDimensions()
   const theme = useTheme("elevated")
   const model = createMemo(
     () =>
@@ -1573,8 +1581,10 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) {
           <span style={{ fg: props.message.error ? theme.text.subdued : local.agent.color(props.message.agent) }}>
             {Locale.titlecase(props.message.agent)}
           </span>
-          <span style={{ fg: theme.text.subdued }}> · {model()}</span>
-          <Show when={duration()}>
+          <Show when={dimensions().width >= 28}>
+            <span style={{ fg: theme.text.subdued }}> · {model()}</span>
+          </Show>
+          <Show when={duration() && (dimensions().width < 28 || dimensions().width >= 36)}>
             <span style={{ fg: theme.text.subdued }}> · {Locale.duration(duration())}</span>
           </Show>
           <Show when={interrupted()}>