Răsfoiți Sursa

Revert "feat: better styling for small screens (short and/or not wide) (#5968)"

This reverts commit ac371d2987762e9b0b7627d7f1ee0ea2b5cab11a.
Dax Raad 3 luni în urmă
părinte
comite
99633cb299

+ 98 - 125
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

@@ -16,7 +16,7 @@ import { usePromptStash } from "./stash"
 import { DialogStash } from "../dialog-stash"
 import { type AutocompleteRef, Autocomplete } from "./autocomplete"
 import { useCommandDialog } from "../dialog-command"
-import { useRenderer, useTerminalDimensions } from "@opentui/solid"
+import { useRenderer } from "@opentui/solid"
 import { Editor } from "@tui/util/editor"
 import { useExit } from "../../context/exit"
 import { Clipboard } from "../../util/clipboard"
@@ -123,9 +123,6 @@ export function Prompt(props: PromptProps) {
   const stash = usePromptStash()
   const command = useCommandDialog()
   const renderer = useRenderer()
-  const dimensions = useTerminalDimensions()
-  const tall = createMemo(() => dimensions().height > 40)
-  const wide = createMemo(() => dimensions().width > 120)
   const { theme, syntax } = useTheme()
 
   function promptModelWarning() {
@@ -949,21 +946,19 @@ export function Prompt(props: PromptProps) {
               cursorColor={theme.text}
               syntaxStyle={syntax()}
             />
-            <Show when={tall()}>
-              <box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
-                <text fg={highlight()}>
-                  {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
-                </text>
-                <Show when={store.mode === "normal"}>
-                  <box flexDirection="row" gap={1}>
-                    <text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
-                      {local.model.parsed().model}
-                    </text>
-                    <text fg={theme.textMuted}>{local.model.parsed().provider}</text>
-                  </box>
-                </Show>
-              </box>
-            </Show>
+            <box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
+              <text fg={highlight()}>
+                {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
+              </text>
+              <Show when={store.mode === "normal"}>
+                <box flexDirection="row" gap={1}>
+                  <text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
+                    {local.model.parsed().model}
+                  </text>
+                  <text fg={theme.textMuted}>{local.model.parsed().provider}</text>
+                </box>
+              </Show>
+            </box>
           </box>
         </box>
         <box
@@ -993,123 +988,101 @@ export function Prompt(props: PromptProps) {
           />
         </box>
         <box flexDirection="row" justifyContent="space-between">
-          <Switch>
-            <Match when={status().type !== "idle"}>
-              <box
-                flexDirection="row"
-                gap={1}
-                flexGrow={1}
-                justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
-              >
-                <box flexShrink={0} flexDirection="row" gap={1}>
-                  {/* @ts-ignore // SpinnerOptions doesn't support marginLeft */}
-                  <spinner marginLeft={1} color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
-                  <box flexDirection="row" gap={1} flexShrink={0}>
-                    {(() => {
-                      const retry = createMemo(() => {
-                        const s = status()
-                        if (s.type !== "retry") return
-                        return s
-                      })
-                      const message = createMemo(() => {
-                        const r = retry()
-                        if (!r) return
-                        if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
-                          return "gemini is way too hot right now"
-                        if (r.message.length > 80) return r.message.slice(0, 80) + "..."
-                        return r.message
-                      })
-                      const isTruncated = createMemo(() => {
-                        const r = retry()
-                        if (!r) return false
-                        return r.message.length > 120
-                      })
-                      const [seconds, setSeconds] = createSignal(0)
-                      onMount(() => {
-                        const timer = setInterval(() => {
-                          const next = retry()?.next
-                          if (next) setSeconds(Math.round((next - Date.now()) / 1000))
-                        }, 1000)
-
-                        onCleanup(() => {
-                          clearTimeout(timer)
-                        })
+          <Show when={status().type !== "idle"} fallback={<text />}>
+            <box
+              flexDirection="row"
+              gap={1}
+              flexGrow={1}
+              justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
+            >
+              <box flexShrink={0} flexDirection="row" gap={1}>
+                {/* @ts-ignore // SpinnerOptions doesn't support marginLeft */}
+                <spinner marginLeft={1} color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
+                <box flexDirection="row" gap={1} flexShrink={0}>
+                  {(() => {
+                    const retry = createMemo(() => {
+                      const s = status()
+                      if (s.type !== "retry") return
+                      return s
+                    })
+                    const message = createMemo(() => {
+                      const r = retry()
+                      if (!r) return
+                      if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
+                        return "gemini is way too hot right now"
+                      if (r.message.length > 80) return r.message.slice(0, 80) + "..."
+                      return r.message
+                    })
+                    const isTruncated = createMemo(() => {
+                      const r = retry()
+                      if (!r) return false
+                      return r.message.length > 120
+                    })
+                    const [seconds, setSeconds] = createSignal(0)
+                    onMount(() => {
+                      const timer = setInterval(() => {
+                        const next = retry()?.next
+                        if (next) setSeconds(Math.round((next - Date.now()) / 1000))
+                      }, 1000)
+
+                      onCleanup(() => {
+                        clearInterval(timer)
                       })
-                      const handleMessageClick = () => {
-                        const r = retry()
-                        if (!r) return
-                        if (isTruncated()) {
-                          DialogAlert.show(dialog, "Retry Error", r.message)
-                        }
+                    })
+                    const handleMessageClick = () => {
+                      const r = retry()
+                      if (!r) return
+                      if (isTruncated()) {
+                        DialogAlert.show(dialog, "Retry Error", r.message)
                       }
+                    }
 
-                      const retryText = () => {
-                        const r = retry()
-                        if (!r) return ""
-                        const baseMessage = message()
-                        const truncatedHint = isTruncated() ? " (click to expand)" : ""
-                        const retryInfo = ` [retrying ${seconds() > 0 ? `in ${seconds()}s ` : ""}attempt #${r.attempt}]`
-                        return baseMessage + truncatedHint + retryInfo
-                      }
+                    const retryText = () => {
+                      const r = retry()
+                      if (!r) return ""
+                      const baseMessage = message()
+                      const truncatedHint = isTruncated() ? " (click to expand)" : ""
+                      const retryInfo = ` [retrying ${seconds() > 0 ? `in ${seconds()}s ` : ""}attempt #${r.attempt}]`
+                      return baseMessage + truncatedHint + retryInfo
+                    }
 
-                      return (
-                        <Show when={retry()}>
-                          <box onMouseUp={handleMessageClick}>
-                            <text fg={theme.error}>{retryText()}</text>
-                          </box>
-                        </Show>
-                      )
-                    })()}
-                  </box>
+                    return (
+                      <Show when={retry()}>
+                        <box onMouseUp={handleMessageClick}>
+                          <text fg={theme.error}>{retryText()}</text>
+                        </box>
+                      </Show>
+                    )
+                  })()}
                 </box>
-                <text fg={store.interrupt > 0 ? theme.primary : theme.text}>
-                  esc{" "}
-                  <span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
-                    {store.interrupt > 0 ? "again to interrupt" : "interrupt"}
-                  </span>
-                </text>
               </box>
-            </Match>
-            <Match when={!tall()}>
-              <box flexDirection="row" gap={1}>
-                <text fg={highlight()}>
-                  {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
-                </text>
-                <Show when={store.mode === "normal"}>
-                  <box flexDirection="row" gap={1}>
-                    <text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
-                      {local.model.parsed().model}
-                    </text>
-                    <text fg={theme.textMuted}>{local.model.parsed().provider}</text>
-                  </box>
-                </Show>
-              </box>
-            </Match>
-          </Switch>
-          <box gap={2} flexDirection="row" marginLeft="auto">
-            <Switch>
-              <Match when={store.mode === "normal"}>
-                <Show when={wide()}>
+              <text fg={store.interrupt > 0 ? theme.primary : theme.text}>
+                esc{" "}
+                <span style={{ fg: store.interrupt > 0 ? theme.primary : theme.textMuted }}>
+                  {store.interrupt > 0 ? "again to interrupt" : "interrupt"}
+                </span>
+              </text>
+            </box>
+          </Show>
+          <Show when={status().type !== "retry"}>
+            <box gap={2} flexDirection="row">
+              <Switch>
+                <Match when={store.mode === "normal"}>
                   <text fg={theme.text}>
                     {keybind.print("agent_cycle")} <span style={{ fg: theme.textMuted }}>switch agent</span>
                   </text>
-                </Show>
-                <Show when={!wide()}>
                   <text fg={theme.text}>
-                    {keybind.print("sidebar_toggle")} <span style={{ fg: theme.textMuted }}>sidebar</span>
+                    {keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
                   </text>
-                </Show>
-                <text fg={theme.text}>
-                  {keybind.print("command_list")} <span style={{ fg: theme.textMuted }}>commands</span>
-                </text>
-              </Match>
-              <Match when={store.mode === "shell"}>
-                <text fg={theme.text}>
-                  esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
-                </text>
-              </Match>
-            </Switch>
-          </box>
+                </Match>
+                <Match when={store.mode === "shell"}>
+                  <text fg={theme.text}>
+                    esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
+                  </text>
+                </Match>
+              </Switch>
+            </box>
+          </Show>
         </box>
       </box>
     </>

+ 91 - 107
packages/opencode/src/cli/cmd/tui/routes/session/header.tsx

@@ -1,140 +1,124 @@
 import { type Accessor, createMemo, Match, Show, Switch } from "solid-js"
 import { useRouteData } from "@tui/context/route"
 import { useSync } from "@tui/context/sync"
+import { pipe, sumBy } from "remeda"
 import { useTheme } from "@tui/context/theme"
-import { EmptyBorder } from "@tui/component/border"
-import type { Session } from "@opencode-ai/sdk/v2"
+import { SplitBorder, EmptyBorder } from "@tui/component/border"
+import type { AssistantMessage, Session } from "@opencode-ai/sdk/v2"
+import { useDirectory } from "../../context/directory"
 import { useKeybind } from "../../context/keybind"
-import { useTerminalDimensions } from "@opentui/solid"
 
-const Title = (props: { session: Accessor<Session>; truncate?: boolean }) => {
+const Title = (props: { session: Accessor<Session> }) => {
   const { theme } = useTheme()
   return (
-    <text fg={theme.text} wrapMode={props.truncate ? "none" : undefined} flexShrink={props.truncate ? 1 : 0}>
+    <text fg={theme.text}>
       <span style={{ bold: true }}>#</span> <span style={{ bold: true }}>{props.session().title}</span>
     </text>
   )
 }
 
+const ContextInfo = (props: { context: Accessor<string | undefined>; cost: Accessor<string> }) => {
+  const { theme } = useTheme()
+  return (
+    <Show when={props.context()}>
+      <text fg={theme.textMuted} wrapMode="none" flexShrink={0}>
+        {props.context()} ({props.cost()})
+      </text>
+    </Show>
+  )
+}
+
 export function Header() {
   const route = useRouteData("session")
   const sync = useSync()
   const session = createMemo(() => sync.session.get(route.sessionID)!)
+  const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
   const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
-  const showShare = createMemo(() => shareEnabled() && !session()?.share?.url)
+
+  const cost = createMemo(() => {
+    const total = pipe(
+      messages(),
+      sumBy((x) => (x.role === "assistant" ? x.cost : 0)),
+    )
+    return new Intl.NumberFormat("en-US", {
+      style: "currency",
+      currency: "USD",
+    }).format(total)
+  })
+
+  const context = createMemo(() => {
+    const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage
+    if (!last) return
+    const total =
+      last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
+    const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID]
+    let result = total.toLocaleString()
+    if (model?.limit.context) {
+      result += "  " + Math.round((total / model.limit.context) * 100) + "%"
+    }
+    return result
+  })
 
   const { theme } = useTheme()
   const keybind = useKeybind()
-  const dimensions = useTerminalDimensions()
-  const tall = createMemo(() => dimensions().height > 40)
 
   return (
     <box flexShrink={0}>
       <box
-        height={1}
-        border={["left"]}
-        borderColor={theme.border}
-        customBorderChars={{
-          ...EmptyBorder,
-          vertical: theme.backgroundPanel.a !== 0 ? "╻" : " ",
-        }}
-      >
-        <box
-          height={1}
-          border={["top"]}
-          borderColor={theme.backgroundPanel}
-          customBorderChars={
-            theme.backgroundPanel.a !== 0
-              ? {
-                  ...EmptyBorder,
-                  horizontal: "▄",
-                }
-              : {
-                  ...EmptyBorder,
-                  horizontal: " ",
-                }
-          }
-        />
-      </box>
-      <box
+        paddingTop={1}
+        paddingBottom={1}
+        paddingLeft={2}
+        paddingRight={1}
+        {...SplitBorder}
         border={["left"]}
         borderColor={theme.border}
-        customBorderChars={{
-          ...EmptyBorder,
-          vertical: "┃",
-          bottomLeft: "╹",
-        }}
+        flexShrink={0}
+        backgroundColor={theme.backgroundPanel}
       >
-        <box
-          paddingTop={tall() ? 1 : 0}
-          paddingBottom={tall() ? 1 : 0}
-          paddingLeft={2}
-          paddingRight={1}
-          flexShrink={0}
-          flexGrow={1}
-          backgroundColor={theme.backgroundPanel}
-        >
-          <Switch>
-            <Match when={session()?.parentID}>
-              <box flexDirection="row" gap={2}>
-                <text fg={theme.text}>
-                  <b>Subagent session</b>
-                </text>
-                <text fg={theme.text}>
-                  Parent <span style={{ fg: theme.textMuted }}>{keybind.print("session_parent")}</span>
-                </text>
-                <text fg={theme.text}>
-                  Prev <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span>
-                </text>
-                <text fg={theme.text}>
-                  Next <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span>
-                </text>
-                <box flexGrow={1} flexShrink={1} />
-                <Show when={showShare()}>
-                  <text fg={theme.textMuted} wrapMode="none" flexShrink={0}>
-                    /share{" "}
-                  </text>
-                </Show>
-              </box>
-            </Match>
-            <Match when={true}>
+        <Switch>
+          <Match when={session()?.parentID}>
+            <box flexDirection="row" gap={2}>
+              <text fg={theme.text}>
+                <b>Subagent session</b>
+              </text>
+              <text fg={theme.text}>
+                Parent <span style={{ fg: theme.textMuted }}>{keybind.print("session_parent")}</span>
+              </text>
+              <text fg={theme.text}>
+                Prev <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle_reverse")}</span>
+              </text>
+              <text fg={theme.text}>
+                Next <span style={{ fg: theme.textMuted }}>{keybind.print("session_child_cycle")}</span>
+              </text>
+              <box flexGrow={1} flexShrink={1} />
+              <ContextInfo context={context} cost={cost} />
+            </box>
+          </Match>
+          <Match when={true}>
+            <box flexDirection="row" justifyContent="space-between" gap={1}>
+              <Title session={session} />
+              <ContextInfo context={context} cost={cost} />
+            </box>
+            <Show when={shareEnabled()}>
               <box flexDirection="row" justifyContent="space-between" gap={1}>
-                <Title session={session} truncate={!tall()} />
-                <Show when={showShare()}>
-                  <text fg={theme.textMuted} wrapMode="none" flexShrink={0}>
-                    /share{" "}
-                  </text>
-                </Show>
+                <box flexGrow={1} flexShrink={1}>
+                  <Switch>
+                    <Match when={session().share?.url}>
+                      <text fg={theme.textMuted} wrapMode="word">
+                        {session().share!.url}
+                      </text>
+                    </Match>
+                    <Match when={true}>
+                      <text fg={theme.text} wrapMode="word">
+                        /share <span style={{ fg: theme.textMuted }}>copy link</span>
+                      </text>
+                    </Match>
+                  </Switch>
+                </box>
               </box>
-            </Match>
-          </Switch>
-        </box>
-      </box>
-      <box
-        height={1}
-        border={["left"]}
-        borderColor={theme.border}
-        customBorderChars={{
-          ...EmptyBorder,
-          vertical: theme.backgroundPanel.a !== 0 ? "╹" : " ",
-        }}
-      >
-        <box
-          height={1}
-          border={["bottom"]}
-          borderColor={theme.backgroundPanel}
-          customBorderChars={
-            theme.backgroundPanel.a !== 0
-              ? {
-                  ...EmptyBorder,
-                  horizontal: "▀",
-                }
-              : {
-                  ...EmptyBorder,
-                  horizontal: " ",
-                }
-          }
-        />
+            </Show>
+          </Match>
+        </Switch>
       </box>
     </box>
   )

+ 4 - 25
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

@@ -22,7 +22,6 @@ import {
   ScrollBoxRenderable,
   addDefaultParsers,
   MacOSScrollAccel,
-  RGBA,
   type ScrollAcceleration,
 } from "@opentui/core"
 import { Prompt, type PromptRef } from "@tui/component/prompt"
@@ -131,15 +130,13 @@ export function Session() {
   const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word")
 
   const wide = createMemo(() => dimensions().width > 120)
-  const tall = createMemo(() => dimensions().height > 40)
   const sidebarVisible = createMemo(() => {
     if (session()?.parentID) return false
     if (sidebar() === "show") return true
     if (sidebar() === "auto" && wide()) return true
     return false
   })
-  const sidebarOverlay = createMemo(() => sidebarVisible() && !wide())
-  const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() && !sidebarOverlay() ? 42 : 0) - 4)
+  const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
 
   const scrollAcceleration = createMemo(() => {
     const tui = sync.data.config.tui
@@ -965,7 +962,7 @@ export function Session() {
       <box flexDirection="row">
         <box flexGrow={1} paddingBottom={1} paddingTop={1} paddingLeft={2} paddingRight={2} gap={1}>
           <Show when={session()}>
-            <Show when={!sidebarVisible() || sidebarOverlay()}>
+            <Show when={!sidebarVisible()}>
               <Header />
             </Show>
             <scrollbox
@@ -1095,33 +1092,15 @@ export function Session() {
                 sessionID={route.sessionID}
               />
             </box>
-            <Show when={(!sidebarVisible() || sidebarOverlay()) && tall()}>
+            <Show when={!sidebarVisible()}>
               <Footer />
             </Show>
           </Show>
           <Toast />
         </box>
-        <Show when={sidebarVisible() && !sidebarOverlay()}>
+        <Show when={sidebarVisible()}>
           <Sidebar sessionID={route.sessionID} />
         </Show>
-        <Show when={sidebarOverlay()}>
-          <box
-            position="absolute"
-            left={0}
-            top={0}
-            width={dimensions().width}
-            height={dimensions().height}
-            backgroundColor={RGBA.fromInts(0, 0, 0, 150)}
-            zIndex={100}
-            flexDirection="row"
-            justifyContent="flex-end"
-            onMouseUp={() => setSidebar("hide")}
-          >
-            <box onMouseUp={(e) => e.stopPropagation()}>
-              <Sidebar sessionID={route.sessionID} />
-            </box>
-          </box>
-        </Show>
       </box>
     </context.Provider>
   )

+ 0 - 1
packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx

@@ -73,7 +73,6 @@ export function Sidebar(props: { sessionID: string }) {
       <box
         backgroundColor={theme.backgroundPanel}
         width={42}
-        height="100%"
         paddingTop={1}
         paddingBottom={1}
         paddingLeft={2}