Procházet zdrojové kódy

wip(share): more styling

Adam před 2 měsíci
rodič
revize
acf1dd8500
30 změnil soubory, kde provedl 264 přidání a 190 odebrání
  1. 1 1
      packages/desktop/src/pages/session.tsx
  2. 0 17
      packages/enterprise/src/app.css
  3. 6 11
      packages/enterprise/src/app.tsx
  4. 169 110
      packages/enterprise/src/routes/share/[sessionID].tsx
  5. 1 1
      packages/opencode/src/bun/index.ts
  6. 1 1
      packages/opencode/src/cli/ui.ts
  7. 1 1
      packages/opencode/src/config/config.ts
  8. 1 1
      packages/opencode/src/config/markdown.ts
  9. 1 1
      packages/opencode/src/file/fzf.ts
  10. 1 1
      packages/opencode/src/file/ripgrep.ts
  11. 1 1
      packages/opencode/src/ide/index.ts
  12. 1 1
      packages/opencode/src/index.ts
  13. 1 1
      packages/opencode/src/installation/index.ts
  14. 1 1
      packages/opencode/src/lsp/client.ts
  15. 1 1
      packages/opencode/src/mcp/index.ts
  16. 1 1
      packages/opencode/src/provider/auth.ts
  17. 1 1
      packages/opencode/src/provider/provider.ts
  18. 1 1
      packages/opencode/src/server/server.ts
  19. 1 1
      packages/opencode/src/session/message-v2.ts
  20. 1 1
      packages/opencode/src/session/message.ts
  21. 1 1
      packages/opencode/src/session/prompt.ts
  22. 1 1
      packages/opencode/src/storage/storage.ts
  23. binární
      packages/ui/src/assets/fonts/tx-02.otf
  24. binární
      packages/ui/src/assets/fonts/tx-02.ttf
  25. binární
      packages/ui/src/assets/fonts/tx-02.woff2
  26. 6 6
      packages/ui/src/components/fonts.tsx
  27. 39 18
      packages/ui/src/components/message-nav.css
  28. 23 7
      packages/ui/src/components/message-nav.tsx
  29. 2 2
      packages/ui/src/styles/theme.css
  30. 0 0
      packages/util/src/error.ts

+ 1 - 1
packages/desktop/src/pages/session.tsx

@@ -339,7 +339,7 @@ export default function Page() {
                     <div class="flex items-start justify-start h-full min-h-0">
                       <Show when={session.messages.user().length > 1}>
                         <MessageNav
-                          classList={{ "mt-1.5 mr-3": wide(), "mt-3 mr-8": !wide() }}
+                          classList={{ "mt-0.5 mr-3 absolute right-full": wide(), "mt-3 mr-8": !wide() }}
                           messages={session.messages.user()}
                           current={session.messages.active()}
                           onMessageSelect={session.messages.setActive}

+ 0 - 17
packages/enterprise/src/app.css

@@ -1,18 +1 @@
 @import "@opencode-ai/ui/styles/tailwind";
-
-:root {
-  --background-rgb: 214, 219, 220;
-  --foreground-rgb: 0, 0, 0;
-}
-
-@media (prefers-color-scheme: dark) {
-  :root {
-    --background-rgb: 0, 0, 0;
-    --foreground-rgb: 255, 255, 255;
-  }
-}
-
-body {
-  /* background: rgb(var(--background-rgb)); */
-  /* color: rgb(var(--foreground-rgb)); */
-}

+ 6 - 11
packages/enterprise/src/app.tsx

@@ -1,6 +1,5 @@
 import { Router } from "@solidjs/router"
 import { FileRoutes } from "@solidjs/start/router"
-import { Suspense } from "solid-js"
 import { Fonts } from "@opencode-ai/ui/fonts"
 import { MetaProvider } from "@solidjs/meta"
 import { MarkedProvider } from "@opencode-ai/ui/context/marked"
@@ -10,16 +9,12 @@ export default function App() {
   return (
     <Router
       root={(props) => (
-        <>
-          <Suspense>
-            <MarkedProvider>
-              <MetaProvider>
-                <Fonts />
-                {props.children}
-              </MetaProvider>
-            </MarkedProvider>
-          </Suspense>
-        </>
+        <MarkedProvider>
+          <MetaProvider>
+            <Fonts />
+            {props.children}
+          </MetaProvider>
+        </MarkedProvider>
       )}
     >
       <FileRoutes />

+ 169 - 110
packages/enterprise/src/routes/share/[sessionID].tsx

@@ -3,15 +3,26 @@ import { SessionTurn } from "@opencode-ai/ui/session-turn"
 import { SessionReview } from "@opencode-ai/ui/session-review"
 import { DataProvider } from "@opencode-ai/ui/context"
 import { createAsync, query, RouteDefinition, useParams } from "@solidjs/router"
-import { createMemo, Show } from "solid-js"
+import { createMemo, ErrorBoundary, Show } from "solid-js"
 import { Share } from "~/core/share"
 import { Logo, Mark } from "@opencode-ai/ui/logo"
 import { IconButton } from "@opencode-ai/ui/icon-button"
 import { iife } from "@opencode-ai/util/iife"
 import { Binary } from "@opencode-ai/util/binary"
+import { NamedError } from "@opencode-ai/util/error"
 import { DateTime } from "luxon"
 import { MessageNav } from "@opencode-ai/ui/message-nav"
 import { createStore } from "solid-js/store"
+import z from "zod"
+import NotFound from "../[...404]"
+
+const SessionDataMissingError = NamedError.create(
+  "SessionDataMissingError",
+  z.object({
+    sessionID: z.string(),
+    message: z.string().optional(),
+  }),
+)
 
 const getData = query(async (sessionID) => {
   const data = await Share.data(sessionID)
@@ -70,6 +81,8 @@ const getData = query(async (sessionID) => {
         break
     }
   }
+  const match = Binary.search(result.session, sessionID!, (s) => s.id)
+  if (!match.found) throw new SessionDataMissingError({ sessionID })
   return result
 }, "getShareData")
 
@@ -80,126 +93,172 @@ export const route = {
 export default function () {
   const params = useParams()
   const data = createAsync(async () => {
-    if (!params.sessionID) return
+    if (!params.sessionID) throw new Error("Missing sessionID")
     return getData(params.sessionID)
   })
 
   return (
-    <Show when={data()}>
-      {(data) => (
-        <DataProvider data={data()}>
-          {iife(() => {
-            const [store, setStore] = createStore({
-              messageId: undefined as string | undefined,
-            })
-            const match = createMemo(() => Binary.search(data().session, params.sessionID!, (s) => s.id))
-            if (!match().found) throw new Error(`Session ${params.sessionID} not found`)
-            const info = createMemo(() => data().session[match().index])
-            const messages = createMemo(() =>
-              params.sessionID ? (data().message[params.sessionID]?.filter((m) => m.role === "user") ?? []) : [],
-            )
-            const firstUserMessage = createMemo(() => messages().at(0))
-            const activeMessage = createMemo(
-              () => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
-            )
-            function setActiveMessage(message: UserMessage | undefined) {
-              if (message) {
-                setStore("messageId", message.id)
-              } else {
-                setStore("messageId", undefined)
+    <ErrorBoundary
+      fallback={(e) => {
+        return (
+          <Show when={e.message === "SessionDataMissingError"}>
+            <NotFound />
+          </Show>
+        )
+      }}
+    >
+      <Show when={data()}>
+        {(data) => (
+          <DataProvider data={data()}>
+            {iife(() => {
+              const [store, setStore] = createStore({
+                messageId: undefined as string | undefined,
+              })
+              const match = createMemo(() => Binary.search(data().session, params.sessionID!, (s) => s.id))
+              if (!match().found) throw new Error(`Session ${params.sessionID} not found`)
+              const info = createMemo(() => data().session[match().index])
+              const messages = createMemo(() =>
+                params.sessionID
+                  ? (data().message[params.sessionID]?.filter((m) => m.role === "user") ?? []).sort(
+                      (a, b) => b.time.created - a.time.created,
+                    )
+                  : [],
+              )
+              const firstUserMessage = createMemo(() => messages().at(0))
+              const activeMessage = createMemo(
+                () => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
+              )
+              function setActiveMessage(message: UserMessage | undefined) {
+                if (message) {
+                  setStore("messageId", message.id)
+                } else {
+                  setStore("messageId", undefined)
+                }
               }
-            }
-            const provider = createMemo(() => activeMessage()?.model?.providerID)
-            const modelID = createMemo(() => activeMessage()?.model?.modelID)
-            const model = createMemo(() => data().model[params.sessionID!]?.find((m) => m.id === modelID()))
-            const diffs = createMemo(() => data().session_diff[params.sessionID!] ?? [])
+              const provider = createMemo(() => activeMessage()?.model?.providerID)
+              const modelID = createMemo(() => activeMessage()?.model?.modelID)
+              const model = createMemo(() => data().model[params.sessionID!]?.find((m) => m.id === modelID()))
+              const diffs = createMemo(() => data().session_diff[params.sessionID!] ?? [])
 
-            return (
-              <div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
-                <header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
-                  <div class="">
-                    <a href="https://opencode.ai">
-                      <Mark />
-                    </a>
-                  </div>
-                  <div class="flex gap-3 items-center">
-                    <IconButton
-                      as={"a"}
-                      href="https://github.com/sst/opencode"
-                      target="_blank"
-                      icon="github"
-                      variant="ghost"
-                    />
-                    <IconButton
-                      as={"a"}
-                      href="https://opencode.ai/discord"
-                      target="_blank"
-                      icon="discord"
-                      variant="ghost"
-                    />
+              const title = (
+                <div class="flex flex-col gap-4 shrink-0">
+                  <div class="h-8 flex gap-4 items-center justify-start self-stretch">
+                    <div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base">
+                      <Mark class="shrink-0 w-3 my-0.5" />
+                      <div class="text-12-mono text-text-base">v{info().version}</div>
+                    </div>
+                    <div class="flex gap-2 items-center">
+                      <img src={`https://models.dev/logos/${provider()}.svg`} class="size-3.5 shrink-0 dark:invert" />
+                      <div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
+                    </div>
+                    <div class="text-12-regular text-text-weaker">
+                      {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
+                    </div>
                   </div>
-                </header>
-                <div class="select-text flex flex-col flex-1 min-h-0">
-                  <div class="w-full flex-1 min-h-0 flex">
-                    <div
-                      classList={{
-                        "@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full mx-auto": true,
-                        "px-21 @4xl:px-6 max-w-2xl": diffs().length > 0,
-                        "px-6 max-w-2xl": diffs().length === 0,
-                      }}
-                    >
-                      <div class="flex flex-col gap-4 shrink-0">
-                        <div class="h-8 flex gap-4 items-center justify-start self-stretch">
-                          <div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base">
-                            <Mark class="shrink-0 w-3 my-0.5" />
-                            <div class="text-12-mono text-text-base">v{info().version}</div>
-                          </div>
-                          <div class="flex gap-2 items-center">
-                            <img
-                              src={`https://models.dev/logos/${provider()}.svg`}
-                              class="size-4 shrink-0 dark:invert"
-                            />
-                            <div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
-                          </div>
-                          <div class="text-12-regular text-text-weaker">
-                            {DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
-                          </div>
+                  <div class="text-left text-16-medium text-text-strong">{info().title}</div>
+                </div>
+              )
+
+              return (
+                <div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
+                  <header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
+                    <div class="">
+                      <a href="https://opencode.ai">
+                        <Mark />
+                      </a>
+                    </div>
+                    <div class="flex gap-3 items-center">
+                      <IconButton
+                        as={"a"}
+                        href="https://github.com/sst/opencode"
+                        target="_blank"
+                        icon="github"
+                        variant="ghost"
+                      />
+                      <IconButton
+                        as={"a"}
+                        href="https://opencode.ai/discord"
+                        target="_blank"
+                        icon="discord"
+                        variant="ghost"
+                      />
+                    </div>
+                  </header>
+                  <div class="select-text flex flex-col flex-1 min-h-0">
+                    <div class="w-full flex-1 min-h-0 flex">
+                      <div
+                        classList={{
+                          "@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full mx-auto": true,
+                          "px-21 @4xl:px-6 max-w-2xl": diffs().length > 0,
+                          "px-6 max-w-2xl": diffs().length === 0,
+                        }}
+                      >
+                        {title}
+                        <div class="flex items-start justify-start h-full min-h-0">
+                          <Show when={messages().length > 1}>
+                            <>
+                              <div
+                                classList={{
+                                  "xl:hidden": true,
+                                  "absolute right-[90%]": diffs().length > 0,
+                                  "absolute right-full": diffs().length === 0,
+                                }}
+                              >
+                                <MessageNav
+                                  classList={{
+                                    "mt-0.5 mr-8": diffs().length === 0,
+                                    "mt-2.5 mr-3": diffs().length > 0,
+                                  }}
+                                  messages={messages()}
+                                  current={activeMessage()}
+                                  onMessageSelect={setActiveMessage}
+                                  size={!diffs().length ? "normal" : "compact"}
+                                />
+                              </div>
+                              <div
+                                classList={{
+                                  "hidden xl:block": true,
+                                  "absolute right-[90%]": diffs().length > 0,
+                                  "absolute right-full": diffs().length === 0,
+                                }}
+                              >
+                                <MessageNav
+                                  classList={{
+                                    "mt-0.5 mr-8": diffs().length === 0,
+                                    "mt-2.5 mr-3": diffs().length > 0,
+                                  }}
+                                  messages={messages()}
+                                  current={activeMessage()}
+                                  onMessageSelect={setActiveMessage}
+                                  size={!diffs().length ? "normal" : "compact"}
+                                />
+                              </div>
+                            </>
+                          </Show>
+                          <SessionTurn
+                            sessionID={params.sessionID!}
+                            messageID={store.messageId ?? firstUserMessage()!.id!}
+                            classes={{ root: "grow", content: "flex flex-col justify-between", container: "pb-20" }}
+                          >
+                            <div class="flex items-center justify-center pb-8 shrink-0">
+                              <Logo class="w-58.5 opacity-12" />
+                            </div>
+                          </SessionTurn>
                         </div>
-                        <div class="text-left text-16-medium text-text-strong">{info().title}</div>
-                      </div>
-                      <div class="flex items-start justify-start h-full min-h-0">
-                        <Show when={messages().length > 1}>
-                          <MessageNav
-                            classList={{ "mt-2 mr-3": true }}
-                            messages={messages()}
-                            current={activeMessage()}
-                            onMessageSelect={setActiveMessage}
-                            size={!diffs().length ? "normal" : "compact"}
-                          />
-                        </Show>
-                        <SessionTurn
-                          sessionID={params.sessionID!}
-                          messageID={store.messageId ?? firstUserMessage()!.id!}
-                          classes={{ root: "grow", content: "flex flex-col justify-between", container: "pb-20" }}
-                        >
-                          <div class="flex items-center justify-center pb-8 shrink-0">
-                            <Logo class="w-58.5 opacity-12" />
-                          </div>
-                        </SessionTurn>
                       </div>
+                      <Show when={diffs().length}>
+                        <div class="relative grow px-6 pt-14 flex-1 min-h-0 border-l border-border-weak-base">
+                          <SessionReview diffs={diffs()} class="pb-20" />
+                        </div>
+                      </Show>
                     </div>
-                    <Show when={diffs().length}>
-                      <div class="relative grow px-6 pt-14 flex-1 min-h-0 border-l border-border-weak-base">
-                        <SessionReview diffs={diffs()} class="pb-20" />
-                      </div>
-                    </Show>
                   </div>
                 </div>
-              </div>
-            )
-          })}
-        </DataProvider>
-      )}
-    </Show>
+              )
+            })}
+          </DataProvider>
+        )}
+      </Show>
+    </ErrorBoundary>
   )
 }

+ 1 - 1
packages/opencode/src/bun/index.ts

@@ -2,7 +2,7 @@ import z from "zod"
 import { Global } from "../global"
 import { Log } from "../util/log"
 import path from "path"
-import { NamedError } from "../util/error"
+import { NamedError } from "@opencode-ai/util/error"
 import { readableStreamToText } from "bun"
 import { createRequire } from "module"
 import { Lock } from "../util/lock"

+ 1 - 1
packages/opencode/src/cli/ui.ts

@@ -1,6 +1,6 @@
 import z from "zod"
 import { EOL } from "os"
-import { NamedError } from "../util/error"
+import { NamedError } from "@opencode-ai/util/error"
 
 export namespace UI {
   const LOGO = [

+ 1 - 1
packages/opencode/src/config/config.ts

@@ -8,7 +8,7 @@ import { mergeDeep, pipe } from "remeda"
 import { Global } from "../global"
 import fs from "fs/promises"
 import { lazy } from "../util/lazy"
-import { NamedError } from "../util/error"
+import { NamedError } from "@opencode-ai/util/error"
 import { Flag } from "../flag/flag"
 import { Auth } from "../auth"
 import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"

+ 1 - 1
packages/opencode/src/config/markdown.ts

@@ -1,4 +1,4 @@
-import { NamedError } from "@/util/error"
+import { NamedError } from "@opencode-ai/util/error"
 import matter from "gray-matter"
 import { z } from "zod"
 

+ 1 - 1
packages/opencode/src/file/fzf.ts

@@ -2,7 +2,7 @@ import path from "path"
 import { Global } from "../global"
 import fs from "fs/promises"
 import z from "zod"
-import { NamedError } from "../util/error"
+import { NamedError } from "@opencode-ai/util/error"
 import { lazy } from "../util/lazy"
 import { Log } from "../util/log"
 import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"

+ 1 - 1
packages/opencode/src/file/ripgrep.ts

@@ -3,7 +3,7 @@ import path from "path"
 import { Global } from "../global"
 import fs from "fs/promises"
 import z from "zod"
-import { NamedError } from "../util/error"
+import { NamedError } from "@opencode-ai/util/error"
 import { lazy } from "../util/lazy"
 import { $ } from "bun"
 

+ 1 - 1
packages/opencode/src/ide/index.ts

@@ -1,6 +1,6 @@
 import { spawn } from "bun"
 import z from "zod"
-import { NamedError } from "../util/error"
+import { NamedError } from "@opencode-ai/util/error"
 import { Log } from "../util/log"
 import { Bus } from "../bus"
 

+ 1 - 1
packages/opencode/src/index.ts

@@ -9,7 +9,7 @@ import { UpgradeCommand } from "./cli/cmd/upgrade"
 import { ModelsCommand } from "./cli/cmd/models"
 import { UI } from "./cli/ui"
 import { Installation } from "./installation"
-import { NamedError } from "./util/error"
+import { NamedError } from "@opencode-ai/util/error"
 import { FormatError } from "./cli/error"
 import { ServeCommand } from "./cli/cmd/serve"
 import { DebugCommand } from "./cli/cmd/debug"

+ 1 - 1
packages/opencode/src/installation/index.ts

@@ -1,7 +1,7 @@
 import path from "path"
 import { $ } from "bun"
 import z from "zod"
-import { NamedError } from "../util/error"
+import { NamedError } from "@opencode-ai/util/error"
 import { Bus } from "../bus"
 import { Log } from "../util/log"
 

+ 1 - 1
packages/opencode/src/lsp/client.ts

@@ -6,7 +6,7 @@ import { LANGUAGE_EXTENSIONS } from "./language"
 import { Bus } from "../bus"
 import z from "zod"
 import type { LSPServer } from "./server"
-import { NamedError } from "../util/error"
+import { NamedError } from "@opencode-ai/util/error"
 import { withTimeout } from "../util/timeout"
 import { Instance } from "../project/instance"
 

+ 1 - 1
packages/opencode/src/mcp/index.ts

@@ -5,7 +5,7 @@ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
 import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
 import { Config } from "../config/config"
 import { Log } from "../util/log"
-import { NamedError } from "../util/error"
+import { NamedError } from "@opencode-ai/util/error"
 import z from "zod/v4"
 import { Instance } from "../project/instance"
 import { withTimeout } from "@/util/timeout"

+ 1 - 1
packages/opencode/src/provider/auth.ts

@@ -4,7 +4,7 @@ import { map, filter, pipe, fromEntries, mapValues } from "remeda"
 import z from "zod"
 import { fn } from "@/util/fn"
 import type { AuthOuathResult, Hooks } from "@opencode-ai/plugin"
-import { NamedError } from "@/util/error"
+import { NamedError } from "@opencode-ai/util/error"
 import { Auth } from "@/auth"
 
 export namespace ProviderAuth {

+ 1 - 1
packages/opencode/src/provider/provider.ts

@@ -7,7 +7,7 @@ import { Log } from "../util/log"
 import { BunProc } from "../bun"
 import { Plugin } from "../plugin"
 import { ModelsDev } from "./models"
-import { NamedError } from "../util/error"
+import { NamedError } from "@opencode-ai/util/error"
 import { Auth } from "../auth"
 import { Instance } from "../project/instance"
 import { Flag } from "../flag/flag"

+ 1 - 1
packages/opencode/src/server/server.ts

@@ -9,7 +9,7 @@ import { Session } from "../session"
 import z from "zod"
 import { Provider } from "../provider/provider"
 import { mapValues } from "remeda"
-import { NamedError } from "../util/error"
+import { NamedError } from "@opencode-ai/util/error"
 import { ModelsDev } from "../provider/models"
 import { Ripgrep } from "../file/ripgrep"
 import { Config } from "../config/config"

+ 1 - 1
packages/opencode/src/session/message-v2.ts

@@ -1,6 +1,6 @@
 import z from "zod"
 import { Bus } from "../bus"
-import { NamedError } from "../util/error"
+import { NamedError } from "@opencode-ai/util/error"
 import { Message } from "./message"
 import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
 import { Identifier } from "../id/id"

+ 1 - 1
packages/opencode/src/session/message.ts

@@ -1,5 +1,5 @@
 import z from "zod"
-import { NamedError } from "../util/error"
+import { NamedError } from "@opencode-ai/util/error"
 
 export namespace Message {
   export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))

+ 1 - 1
packages/opencode/src/session/prompt.ts

@@ -43,7 +43,7 @@ import { Command } from "../command"
 import { $, fileURLToPath } from "bun"
 import { ConfigMarkdown } from "../config/markdown"
 import { SessionSummary } from "./summary"
-import { NamedError } from "@/util/error"
+import { NamedError } from "@opencode-ai/util/error"
 import { fn } from "@/util/fn"
 import { SessionProcessor } from "./processor"
 import { TaskTool } from "@/tool/task"

+ 1 - 1
packages/opencode/src/storage/storage.ts

@@ -5,7 +5,7 @@ import { Global } from "../global"
 import { lazy } from "../util/lazy"
 import { Lock } from "../util/lock"
 import { $ } from "bun"
-import { NamedError } from "@/util/error"
+import { NamedError } from "@opencode-ai/util/error"
 import z from "zod"
 
 export namespace Storage {

binární
packages/ui/src/assets/fonts/tx-02.otf


binární
packages/ui/src/assets/fonts/tx-02.ttf


binární
packages/ui/src/assets/fonts/tx-02.woff2


+ 6 - 6
packages/ui/src/components/fonts.tsx

@@ -1,6 +1,6 @@
 import { Style, Link } from "@solidjs/meta"
 import geist from "../assets/fonts/geist.woff2"
-import geistMono from "../assets/fonts/geist-mono.woff2"
+import tx02 from "../assets/fonts/tx-02.woff2"
 
 export const Fonts = () => {
   return (
@@ -22,14 +22,14 @@ export const Fonts = () => {
           line-gap-override: 1%;
         }
         @font-face {
-          font-family: "Geist Mono";
-          src: url("${geistMono}") format("woff2-variations");
+          font-family: "Berkeley Mono";
+          src: url("${tx02}") format("woff2-variations");
           font-display: swap;
           font-style: normal;
-          font-weight: 100 900;
+          font-weight: 400 700;
         }
         @font-face {
-          font-family: "Geist Mono Fallback";
+          font-family: "Berkeley Mono Fallback";
           src: local("Courier New");
           size-adjust: 100%;
           ascent-override: 97%;
@@ -38,7 +38,7 @@ export const Fonts = () => {
         }
       `}</Style>
       <Link rel="preload" href={geist} as="font" type="font/woff2" crossorigin="anonymous" />
-      <Link rel="preload" href={geistMono} as="font" type="font/woff2" crossorigin="anonymous" />
+      <Link rel="preload" href={tx02} as="font" type="font/woff2" crossorigin="anonymous" />
     </>
   )
 }

+ 39 - 18
packages/ui/src/components/message-nav.css

@@ -1,6 +1,4 @@
 [data-component="message-nav"] {
-  /* margin-right: 32px; */
-  /* margin-top: 12px; */
   flex-shrink: 0;
   display: flex;
   flex-direction: column;
@@ -9,15 +7,8 @@
   list-style: none;
 
   &[data-size="normal"] {
-    position: absolute;
-    right: 100%;
     width: 240px;
-    /* margin-top: 12px; */
-
-    @media (min-width: 80rem) {
-      gap: 8px;
-      /* margin-top: 4px; */
-    }
+    gap: 4px;
   }
 }
 
@@ -36,10 +27,8 @@
   display: flex;
   align-items: center;
   justify-content: flex-start;
-  height: 8px;
-  width: 32px;
-  /* margin-right: -12px; */
-  cursor: pointer;
+  height: 12px;
+  width: 24px;
   border: none;
   background: none;
   padding: 0;
@@ -52,7 +41,7 @@
 
 [data-slot="message-nav-tick-line"] {
   height: 1px;
-  width: 20px;
+  width: 16px;
   background-color: var(--icon-base);
   transition:
     width 0.2s,
@@ -69,11 +58,12 @@
   align-items: center;
   align-self: stretch;
   width: 100%;
-  column-gap: 8px;
+  column-gap: 12px;
   cursor: default;
   border: none;
   background: none;
-  padding: 0;
+  padding: 4px 12px;
+  border-radius: var(--radius-sm);
 }
 
 [data-slot="message-nav-title-preview"] {
@@ -90,6 +80,37 @@
   }
 }
 
-[data-slot="message-nav-item"]:hover [data-slot="message-nav-title-preview"] {
+[data-slot="message-nav-item"]:hover [data-slot="message-nav-message-button"] {
+  background-color: var(--surface-base);
+}
+[data-slot="message-nav-item"]:active [data-slot="message-nav-message-button"] {
+  background-color: var(--surface-base-active);
+}
+
+[data-slot="message-nav-item"]:active [data-slot="message-nav-title-preview"] {
   color: var(--text-base);
 }
+
+[data-slot="message-nav-tooltip"] {
+  z-index: 1000;
+}
+
+[data-slot="message-nav-tooltip-content"] {
+  display: flex;
+  padding: 4px 4px 6px 4px;
+  justify-content: center;
+  align-items: center;
+  border-radius: var(--radius-md);
+  background: var(--surface-raised-stronger-non-alpha);
+
+  /* border/shadow-xs/base */
+  box-shadow:
+    0 0 0 1px var(--border-weak-base, rgba(17, 0, 0, 0.12)),
+    0 1px 2px -1px rgba(19, 16, 16, 0.04),
+    0 1px 2px 0 rgba(19, 16, 16, 0.06),
+    0 1px 3px 0 rgba(19, 16, 16, 0.08);
+
+  * {
+    margin: 0 !important;
+  }
+}

+ 23 - 7
packages/ui/src/components/message-nav.tsx

@@ -2,6 +2,8 @@ import { UserMessage } from "@opencode-ai/sdk"
 import { ComponentProps, createMemo, For, Match, Show, splitProps, Switch } from "solid-js"
 import { DiffChanges } from "./diff-changes"
 import { Spinner } from "./spinner"
+import { HoverCard } from "@kobalte/core/hover-card"
+import { Tooltip } from "@kobalte/core/tooltip"
 
 export function MessageNav(
   props: ComponentProps<"ul"> & {
@@ -17,7 +19,7 @@ export function MessageNav(
     return local.messages?.at(0)
   })
 
-  return (
+  const content = (
     <ul role="list" data-component="message-nav" data-size={local.size} {...others}>
       <For each={local.messages}>
         {(message) => {
@@ -28,13 +30,9 @@ export function MessageNav(
             <li data-slot="message-nav-item">
               <Switch>
                 <Match when={local.size === "compact"}>
-                  <button
-                    data-slot="message-nav-tick-button"
-                    data-active={message.id === local.current?.id || undefined}
-                    onClick={handleClick}
-                  >
+                  <div data-slot="message-nav-tick-button" data-active={message.id === local.current?.id || undefined}>
                     <div data-slot="message-nav-tick-line" />
-                  </button>
+                  </div>
                 </Match>
                 <Match when={local.size === "normal"}>
                   <button data-slot="message-nav-message-button" onClick={handleClick}>
@@ -63,4 +61,22 @@ export function MessageNav(
       </For>
     </ul>
   )
+
+  return (
+    <Switch>
+      <Match when={local.size === "compact"}>
+        <Tooltip openDelay={0} closeDelay={0} placement="top-start" gutter={-65} shift={-16} overlap>
+          <Tooltip.Trigger as="div">{content}</Tooltip.Trigger>
+          <Tooltip.Portal>
+            <Tooltip.Content data-slot="message-nav-tooltip">
+              <div data-slot="message-nav-tooltip-content">
+                <MessageNav {...props} size="normal" />
+              </div>
+            </Tooltip.Content>
+          </Tooltip.Portal>
+        </Tooltip>
+      </Match>
+      <Match when={local.size === "normal"}>{content}</Match>
+    </Switch>
+  )
 }

+ 2 - 2
packages/ui/src/styles/theme.css

@@ -1,8 +1,8 @@
 :root {
   --font-family-sans: "Geist", "Geist Fallback";
   --font-family-sans--font-feature-settings: "ss03" 1;
-  --font-family-mono: "Geist Mono", "Geist Mono Fallback";
-  --font-family-mono--font-feature-settings: "ss02" 1;
+  --font-family-mono: "Berkeley Mono", "Berkeley Mono Fallback";
+  --font-family-mono--font-feature-settings: "ss01" 1;
 
   --font-size-small: 12px;
   --font-size-base: 14px;

+ 0 - 0
packages/opencode/src/util/error.ts → packages/util/src/error.ts