Преглед изворни кода

Merge remote-tracking branch 'origin/dev' into fix/google-vertex-anthropic-thinking

Aiden Cline пре 2 месеци
родитељ
комит
87f0b68f5a
65 измењених фајлова са 717 додато и 346 уклоњено
  1. 23 18
      bun.lock
  2. 4 4
      nix/hashes.json
  3. 1 1
      packages/app/package.json
  4. 100 7
      packages/app/src/components/dialog-select-model.tsx
  5. 137 99
      packages/app/src/components/dialog-select-server.tsx
  6. 12 4
      packages/app/src/components/prompt-input.tsx
  7. 8 5
      packages/app/src/components/session/session-header.tsx
  8. 1 13
      packages/app/src/components/session/session-new-view.tsx
  9. 90 49
      packages/app/src/components/status-popover.tsx
  10. 6 0
      packages/app/src/components/terminal.tsx
  11. 2 1
      packages/app/src/components/titlebar.tsx
  12. 7 0
      packages/app/src/context/comments.tsx
  13. 11 7
      packages/app/src/context/terminal.tsx
  14. 33 78
      packages/app/src/pages/session.tsx
  15. 1 1
      packages/console/app/package.json
  16. 1 1
      packages/console/core/package.json
  17. 1 1
      packages/console/function/package.json
  18. 1 1
      packages/console/mail/package.json
  19. 1 1
      packages/desktop/package.json
  20. 1 1
      packages/enterprise/package.json
  21. 6 6
      packages/extensions/zed/extension.toml
  22. 1 1
      packages/function/package.json
  23. 1 1
      packages/opencode/package.json
  24. 2 2
      packages/opencode/src/question/index.ts
  25. 1 1
      packages/plugin/package.json
  26. 2 2
      packages/sdk/js/package.json
  27. 22 2
      packages/sdk/js/src/v2/gen/sdk.gen.ts
  28. 2 4
      packages/sdk/openapi.json
  29. 1 1
      packages/slack/package.json
  30. 2 1
      packages/ui/package.json
  31. 1 1
      packages/ui/src/components/dialog.css
  32. 1 0
      packages/ui/src/components/icon.tsx
  33. 4 0
      packages/ui/src/components/line-comment.css
  34. 1 0
      packages/ui/src/components/line-comment.tsx
  35. 0 1
      packages/ui/src/components/list.css
  36. 2 0
      packages/ui/src/components/list.tsx
  37. 49 6
      packages/ui/src/components/markdown.tsx
  38. 98 3
      packages/ui/src/components/popover.tsx
  39. 1 1
      packages/ui/src/components/session-review.css
  40. 7 1
      packages/ui/src/components/tabs.css
  41. 1 1
      packages/ui/src/components/text-field.css
  42. 7 4
      packages/ui/src/components/text-field.tsx
  43. 1 1
      packages/ui/src/components/tooltip.css
  44. 11 2
      packages/ui/src/components/tooltip.tsx
  45. 21 1
      packages/ui/src/hooks/create-auto-scroll.tsx
  46. 6 0
      packages/ui/src/hooks/use-filtered-list.tsx
  47. 1 0
      packages/ui/src/i18n/ar.ts
  48. 1 0
      packages/ui/src/i18n/br.ts
  49. 1 0
      packages/ui/src/i18n/da.ts
  50. 1 0
      packages/ui/src/i18n/de.ts
  51. 1 0
      packages/ui/src/i18n/en.ts
  52. 1 0
      packages/ui/src/i18n/es.ts
  53. 1 0
      packages/ui/src/i18n/fr.ts
  54. 1 0
      packages/ui/src/i18n/ja.ts
  55. 1 0
      packages/ui/src/i18n/ko.ts
  56. 1 0
      packages/ui/src/i18n/no.ts
  57. 1 0
      packages/ui/src/i18n/pl.ts
  58. 1 0
      packages/ui/src/i18n/ru.ts
  59. 1 0
      packages/ui/src/i18n/zh.ts
  60. 1 0
      packages/ui/src/i18n/zht.ts
  61. 4 4
      packages/ui/src/styles/theme.css
  62. 4 4
      packages/ui/src/theme/themes/oc-1.json
  63. 1 1
      packages/util/package.json
  64. 1 1
      packages/web/package.json
  65. 1 1
      sdks/vscode/package.json

+ 23 - 18
bun.lock

@@ -23,7 +23,7 @@
     },
     "packages/app": {
       "name": "@opencode-ai/app",
-      "version": "1.1.34",
+      "version": "1.1.35",
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
@@ -73,7 +73,7 @@
     },
     "packages/console/app": {
       "name": "@opencode-ai/console-app",
-      "version": "1.1.34",
+      "version": "1.1.35",
       "dependencies": {
         "@cloudflare/vite-plugin": "1.15.2",
         "@ibm/plex": "6.4.1",
@@ -107,7 +107,7 @@
     },
     "packages/console/core": {
       "name": "@opencode-ai/console-core",
-      "version": "1.1.34",
+      "version": "1.1.35",
       "dependencies": {
         "@aws-sdk/client-sts": "3.782.0",
         "@jsx-email/render": "1.1.1",
@@ -134,7 +134,7 @@
     },
     "packages/console/function": {
       "name": "@opencode-ai/console-function",
-      "version": "1.1.34",
+      "version": "1.1.35",
       "dependencies": {
         "@ai-sdk/anthropic": "2.0.0",
         "@ai-sdk/openai": "2.0.2",
@@ -158,7 +158,7 @@
     },
     "packages/console/mail": {
       "name": "@opencode-ai/console-mail",
-      "version": "1.1.34",
+      "version": "1.1.35",
       "dependencies": {
         "@jsx-email/all": "2.2.3",
         "@jsx-email/cli": "1.4.3",
@@ -182,7 +182,7 @@
     },
     "packages/desktop": {
       "name": "@opencode-ai/desktop",
-      "version": "1.1.34",
+      "version": "1.1.35",
       "dependencies": {
         "@opencode-ai/app": "workspace:*",
         "@opencode-ai/ui": "workspace:*",
@@ -211,7 +211,7 @@
     },
     "packages/enterprise": {
       "name": "@opencode-ai/enterprise",
-      "version": "1.1.34",
+      "version": "1.1.35",
       "dependencies": {
         "@opencode-ai/ui": "workspace:*",
         "@opencode-ai/util": "workspace:*",
@@ -240,7 +240,7 @@
     },
     "packages/function": {
       "name": "@opencode-ai/function",
-      "version": "1.1.34",
+      "version": "1.1.35",
       "dependencies": {
         "@octokit/auth-app": "8.0.1",
         "@octokit/rest": "catalog:",
@@ -256,7 +256,7 @@
     },
     "packages/opencode": {
       "name": "opencode",
-      "version": "1.1.34",
+      "version": "1.1.35",
       "bin": {
         "opencode": "./bin/opencode",
       },
@@ -360,7 +360,7 @@
     },
     "packages/plugin": {
       "name": "@opencode-ai/plugin",
-      "version": "1.1.34",
+      "version": "1.1.35",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "zod": "catalog:",
@@ -380,9 +380,9 @@
     },
     "packages/sdk/js": {
       "name": "@opencode-ai/sdk",
-      "version": "1.1.34",
+      "version": "1.1.35",
       "devDependencies": {
-        "@hey-api/openapi-ts": "0.90.4",
+        "@hey-api/openapi-ts": "0.90.10",
         "@tsconfig/node22": "catalog:",
         "@types/node": "catalog:",
         "@typescript/native-preview": "catalog:",
@@ -391,7 +391,7 @@
     },
     "packages/slack": {
       "name": "@opencode-ai/slack",
-      "version": "1.1.34",
+      "version": "1.1.35",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "@slack/bolt": "^3.17.1",
@@ -404,7 +404,7 @@
     },
     "packages/ui": {
       "name": "@opencode-ai/ui",
-      "version": "1.1.34",
+      "version": "1.1.35",
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
@@ -423,6 +423,7 @@
         "marked": "catalog:",
         "marked-katex-extension": "5.1.6",
         "marked-shiki": "catalog:",
+        "morphdom": "2.7.8",
         "remeda": "catalog:",
         "shiki": "catalog:",
         "solid-js": "catalog:",
@@ -445,7 +446,7 @@
     },
     "packages/util": {
       "name": "@opencode-ai/util",
-      "version": "1.1.34",
+      "version": "1.1.35",
       "dependencies": {
         "zod": "catalog:",
       },
@@ -456,7 +457,7 @@
     },
     "packages/web": {
       "name": "@opencode-ai/web",
-      "version": "1.1.34",
+      "version": "1.1.35",
       "dependencies": {
         "@astrojs/cloudflare": "12.6.3",
         "@astrojs/markdown-remark": "6.3.1",
@@ -928,11 +929,13 @@
 
     "@happy-dom/global-registrator": ["@happy-dom/[email protected]", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
 
-    "@hey-api/codegen-core": ["@hey-api/[email protected].2", "", { "dependencies": { "ansi-colors": "4.1.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-88cqrrB2cLXN8nMOHidQTcVOnZsJ5kebEbBefjMCifaUCwTA30ouSSWvTZqrOX4O104zjJyu7M8Gcv/NNYQuaA=="],
+    "@hey-api/codegen-core": ["@hey-api/[email protected].5", "", { "dependencies": { "@hey-api/types": "0.1.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-f2ZHucnA2wBGAY8ipB4wn/mrEYW+WUxU2huJmUvfDO6AE2vfILSHeF3wCO39Pz4wUYPoAWZByaauftLrOfC12Q=="],
 
     "@hey-api/json-schema-ref-parser": ["@hey-api/[email protected]", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA=="],
 
-    "@hey-api/openapi-ts": ["@hey-api/[email protected]", "", { "dependencies": { "@hey-api/codegen-core": "^0.5.2", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-9l++kjcb0ui4JqPlueZ6OZ9zKn6eK/8//Z2jHcIXb5MRwDRgubOOSpTU5llEv3uvWfT10VzcMp99dySWq0AASw=="],
+    "@hey-api/openapi-ts": ["@hey-api/[email protected]", "", { "dependencies": { "@hey-api/codegen-core": "^0.5.5", "@hey-api/json-schema-ref-parser": "1.2.2", "@hey-api/types": "0.1.2", "ansi-colors": "4.1.3", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-o0wlFxuLt1bcyIV/ZH8DQ1wrgODTnUYj/VfCHOOYgXUQlLp9Dm2PjihOz+WYrZLowhqUhSKeJRArOGzvLuOTsg=="],
+
+    "@hey-api/types": ["@hey-api/[email protected]", "", {}, "sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA=="],
 
     "@hono/node-server": ["@hono/[email protected]", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
 
@@ -3102,6 +3105,8 @@
 
     "mkdirp": ["[email protected]", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
 
+    "morphdom": ["[email protected]", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="],
+
     "mrmime": ["[email protected]", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
 
     "ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],

+ 4 - 4
nix/hashes.json

@@ -1,8 +1,8 @@
 {
   "nodeModules": {
-    "x86_64-linux": "sha256-spng4G0hikHyNhplZiXL/K4XzSBsQNwFCrtzatuY+e4=",
-    "aarch64-linux": "sha256-j6kTzWlSPWwoyvOR6nJAx8VpikMsy/U3ZAJGI2PxPE0=",
-    "aarch64-darwin": "sha256-H4XA72cYxaBBpT6wYAAAOhW4gHduUTxiqT/cNCjW5zc=",
-    "x86_64-darwin": "sha256-4pljrneppSsxS5EYoXX0914L9z9jLHcKatm5T8L6/54="
+    "x86_64-linux": "sha256-olTZ+tKugAY3LxizsJMlbK3TW78HZUoM03PigvQLP4A=",
+    "aarch64-linux": "sha256-xdKDeqMEnYM2+vGySfb8pbcYyo/xMmgxG/ZhPCKaZEg=",
+    "aarch64-darwin": "sha256-fihCTrHIiUG+py4vuqdr+YshqSKm2/B5onY50b97sPM=",
+    "x86_64-darwin": "sha256-inlQQPNAOdkmKK6HQAMI2bG/ZFlfwmUQu9a6vm6Q0jQ="
   }
 }

+ 1 - 1
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/app",
-  "version": "1.1.34",
+  "version": "1.1.35",
   "description": "",
   "type": "module",
   "exports": {

+ 100 - 7
packages/app/src/components/dialog-select-model.tsx

@@ -1,5 +1,6 @@
 import { Popover as Kobalte } from "@kobalte/core/popover"
-import { Component, ComponentProps, createMemo, createSignal, JSX, Show, ValidComponent } from "solid-js"
+import { Component, ComponentProps, createEffect, createMemo, JSX, onCleanup, Show, ValidComponent } from "solid-js"
+import { createStore } from "solid-js/store"
 import { useLocal } from "@/context/local"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { popularProviders } from "@/hooks/use-providers"
@@ -92,26 +93,118 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
   triggerAs?: T
   triggerProps?: ComponentProps<T>
 }) {
-  const [open, setOpen] = createSignal(false)
+  const [store, setStore] = createStore<{
+    open: boolean
+    dismiss: "escape" | "outside" | null
+    trigger?: HTMLElement
+    content?: HTMLElement
+  }>({
+    open: false,
+    dismiss: null,
+    trigger: undefined,
+    content: undefined,
+  })
   const dialog = useDialog()
 
   const handleManage = () => {
-    setOpen(false)
+    setStore("open", false)
     dialog.show(() => <DialogManageModels />)
   }
   const language = useLanguage()
 
+  createEffect(() => {
+    if (!store.open) return
+
+    const inside = (node: Node | null | undefined) => {
+      if (!node) return false
+      const el = store.content
+      if (el && el.contains(node)) return true
+      const anchor = store.trigger
+      if (anchor && anchor.contains(node)) return true
+      return false
+    }
+
+    const onKeyDown = (event: KeyboardEvent) => {
+      if (event.key !== "Escape") return
+      setStore("dismiss", "escape")
+      setStore("open", false)
+      event.preventDefault()
+      event.stopPropagation()
+    }
+
+    const onPointerDown = (event: PointerEvent) => {
+      const target = event.target
+      if (!(target instanceof Node)) return
+      if (inside(target)) return
+      setStore("dismiss", "outside")
+      setStore("open", false)
+    }
+
+    const onFocusIn = (event: FocusEvent) => {
+      if (!store.content) return
+      const target = event.target
+      if (!(target instanceof Node)) return
+      if (inside(target)) return
+      setStore("dismiss", "outside")
+      setStore("open", false)
+    }
+
+    window.addEventListener("keydown", onKeyDown, true)
+    window.addEventListener("pointerdown", onPointerDown, true)
+    window.addEventListener("focusin", onFocusIn, true)
+
+    onCleanup(() => {
+      window.removeEventListener("keydown", onKeyDown, true)
+      window.removeEventListener("pointerdown", onPointerDown, true)
+      window.removeEventListener("focusin", onFocusIn, true)
+    })
+  })
+
   return (
-    <Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
-      <Kobalte.Trigger as={props.triggerAs ?? "div"} {...(props.triggerProps as any)}>
+    <Kobalte
+      open={store.open}
+      onOpenChange={(next) => {
+        if (next) setStore("dismiss", null)
+        setStore("open", next)
+      }}
+      modal={false}
+      placement="top-start"
+      gutter={8}
+    >
+      <Kobalte.Trigger
+        ref={(el) => setStore("trigger", el)}
+        as={props.triggerAs ?? "div"}
+        {...(props.triggerProps as any)}
+      >
         {props.children}
       </Kobalte.Trigger>
       <Kobalte.Portal>
-        <Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden">
+        <Kobalte.Content
+          ref={(el) => setStore("content", el)}
+          class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
+          onEscapeKeyDown={(event) => {
+            setStore("dismiss", "escape")
+            setStore("open", false)
+            event.preventDefault()
+            event.stopPropagation()
+          }}
+          onPointerDownOutside={() => {
+            setStore("dismiss", "outside")
+            setStore("open", false)
+          }}
+          onFocusOutside={() => {
+            setStore("dismiss", "outside")
+            setStore("open", false)
+          }}
+          onCloseAutoFocus={(event) => {
+            if (store.dismiss === "outside") event.preventDefault()
+            setStore("dismiss", null)
+          }}
+        >
           <Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title>
           <ModelList
             provider={props.provider}
-            onSelect={() => setOpen(false)}
+            onSelect={() => setStore("open", false)}
             class="p-1"
             action={
               <IconButton

+ 137 - 99
packages/app/src/components/dialog-select-server.tsx

@@ -11,7 +11,8 @@ import { usePlatform } from "@/context/platform"
 import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
 import { useNavigate } from "@solidjs/router"
 import { useLanguage } from "@/context/language"
-import { Popover } from "@opencode-ai/ui/popover"
+import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
+import { Tooltip } from "@opencode-ai/ui/tooltip"
 import { useGlobalSDK } from "@/context/global-sdk"
 
 type ServerStatus = { healthy: boolean; version?: string }
@@ -52,16 +53,27 @@ async function checkHealth(url: string, platform: ReturnType<typeof usePlatform>
 
 function AddRow(props: AddRowProps) {
   return (
-    <div class="flex items-center gap-3 px-4 min-w-0 flex-1">
-      <div
-        classList={{
-          "size-1.5 rounded-full shrink-0": true,
-          "bg-icon-success-base": props.status === true,
-          "bg-icon-critical-base": props.status === false,
-          "bg-border-weak-base": props.status === undefined,
-        }}
-      />
-      <div class="flex-1 min-w-0">
+    <div class="flex items-center px-4 min-h-14 py-3 min-w-0 flex-1">
+      <div class="flex-1 min-w-0 [&_[data-slot=input-wrapper]]:relative">
+        <div
+          classList={{
+            "size-1.5 rounded-full absolute left-3 z-10 pointer-events-none": true,
+            "bg-icon-success-base": props.status === true,
+            "bg-icon-critical-base": props.status === false,
+            "bg-border-weak-base": props.status === undefined,
+          }}
+          style={{ top: "50%", transform: "translateY(-50%)" }}
+          ref={(el) => {
+            // Position relative to input-wrapper
+            requestAnimationFrame(() => {
+              const wrapper = el.parentElement?.querySelector('[data-slot="input-wrapper"]')
+              if (wrapper instanceof HTMLElement) {
+                wrapper.style.position = "relative"
+                wrapper.appendChild(el)
+              }
+            })
+          }}
+        />
         <TextField
           type="text"
           hideLabel
@@ -74,6 +86,7 @@ function AddRow(props: AddRowProps) {
           onChange={props.onChange}
           onKeyDown={props.onKeyDown}
           onBlur={props.onBlur}
+          class="pl-7"
         />
       </div>
     </div>
@@ -134,7 +147,14 @@ export function DialogSelectServer() {
       status: undefined as boolean | undefined,
     },
   })
-  const [defaultUrl, defaultUrlActions] = createResource(() => platform.getDefaultServerUrl?.())
+  const [defaultUrl, defaultUrlActions] = createResource(
+    async () => {
+      const url = await platform.getDefaultServerUrl?.()
+      if (!url) return null
+      return normalizeServerUrl(url) ?? null
+    },
+    { initialValue: null },
+  )
   const isDesktop = platform.platform === "desktop"
 
   const looksComplete = (value: string) => {
@@ -344,17 +364,23 @@ export function DialogSelectServer() {
 
   return (
     <Dialog title={language.t("dialog.server.title")}>
-      <div class="flex flex-col gap-2 pb-4">
+      <div class="flex flex-col gap-2">
         <List
-          search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: true }}
+          search={{ placeholder: language.t("dialog.server.search.placeholder"), autofocus: false }}
+          noInitialSelection
           emptyMessage={language.t("dialog.server.empty")}
           items={sortedItems}
           key={(x) => x}
           onSelect={(x) => {
             if (x) select(x)
           }}
+          onFilter={(value) => {
+            if (value && store.addServer.showForm && !store.addServer.adding) {
+              resetAdd()
+            }
+          }}
           divider={true}
-          class="[&_[data-slot=list-scroll]]:max-h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-raised-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:py-3"
+          class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:max-h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-raised-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent [&_[data-slot=list-item-add]]:px-0"
           add={
             store.addServer.showForm
               ? {
@@ -375,7 +401,35 @@ export function DialogSelectServer() {
           }
         >
           {(i) => {
-            const [popoverOpen, setPopoverOpen] = createSignal(false)
+            const [truncated, setTruncated] = createSignal(false)
+            let nameRef: HTMLSpanElement | undefined
+            let versionRef: HTMLSpanElement | undefined
+
+            const check = () => {
+              const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false
+              const versionTruncated = versionRef ? versionRef.scrollWidth > versionRef.clientWidth : false
+              setTruncated(nameTruncated || versionTruncated)
+            }
+
+            createEffect(() => {
+              check()
+              window.addEventListener("resize", check)
+              onCleanup(() => window.removeEventListener("resize", check))
+            })
+
+            const tooltipValue = () => {
+              const name = serverDisplayName(i)
+              const version = store.status[i]?.version
+              return (
+                <span class="flex items-center gap-2">
+                  <span>{name}</span>
+                  <Show when={version}>
+                    <span class="text-text-invert-base">{version}</span>
+                  </Show>
+                </span>
+              )
+            }
+
             return (
               <div class="flex items-center gap-3 min-w-0 flex-1 group/item">
                 <Show
@@ -393,58 +447,54 @@ export function DialogSelectServer() {
                     />
                   }
                 >
-                  <div
-                    class="flex items-center gap-3 px-4 min-w-0 flex-1"
-                    classList={{ "opacity-50": store.status[i]?.healthy === false }}
-                  >
+                  <Tooltip value={tooltipValue()} placement="top" inactive={!truncated()}>
                     <div
-                      classList={{
-                        "size-1.5 rounded-full shrink-0": true,
-                        "bg-icon-success-base": store.status[i]?.healthy === true,
-                        "bg-icon-critical-base": store.status[i]?.healthy === false,
-                        "bg-border-weak-base": store.status[i] === undefined,
-                      }}
-                    />
-                    <span class="truncate">{serverDisplayName(i)}</span>
-                    <Show when={store.status[i]?.version}>
-                      <span class="text-text-weak text-14-regular">{store.status[i]?.version}</span>
-                    </Show>
-                    <Show when={defaultUrl() === i}>
-                      <span class="text-text-weak bg-surface-base text-14-regular px-1.5 rounded-xs">
-                        {language.t("dialog.server.status.default")}
+                      class="flex items-center gap-3 px-4 min-w-0 flex-1"
+                      classList={{ "opacity-50": store.status[i]?.healthy === false }}
+                    >
+                      <div
+                        classList={{
+                          "size-1.5 rounded-full shrink-0": true,
+                          "bg-icon-success-base": store.status[i]?.healthy === true,
+                          "bg-icon-critical-base": store.status[i]?.healthy === false,
+                          "bg-border-weak-base": store.status[i] === undefined,
+                        }}
+                      />
+                      <span ref={nameRef} class="truncate">
+                        {serverDisplayName(i)}
                       </span>
-                    </Show>
-                  </div>
+                      <Show when={store.status[i]?.version}>
+                        <span ref={versionRef} class="text-text-weak text-14-regular truncate">
+                          {store.status[i]?.version}
+                        </span>
+                      </Show>
+                      <Show when={defaultUrl() === i}>
+                        <span class="text-text-weak bg-surface-base text-14-regular px-1.5 rounded-xs">
+                          {language.t("dialog.server.status.default")}
+                        </span>
+                      </Show>
+                    </div>
+                  </Tooltip>
                 </Show>
                 <Show when={store.editServer.id !== i}>
-                  <div class="flex items-center justify-center gap-5 px-4">
+                  <div class="flex items-center justify-center gap-5 pl-4">
                     <Show when={current() === i}>
                       <p class="text-text-weak text-12-regular">{language.t("dialog.server.current")}</p>
                     </Show>
 
-                    <div onClick={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}>
-                      <Popover
-                        open={popoverOpen()}
-                        onOpenChange={setPopoverOpen}
-                        placement="bottom-start"
-                        trigger={
-                          <IconButton
-                            icon="dot-grid"
-                            variant="ghost"
-                            class="bg-transparent transition-opacity shrink-0 hover:scale-110 size-8"
-                            onPointerDown={(event: PointerEvent) => event.stopPropagation()}
-                          />
-                        }
-                        class="w-max !min-w-fit !max-w-none"
-                      >
-                        <div class="flex flex-col gap-1">
-                          <Button
-                            variant="ghost"
-                            size="normal"
-                            class="justify-start text-md"
-                            onClick={(e: MouseEvent) => {
-                              e.stopPropagation()
-                              setPopoverOpen(false)
+                    <DropdownMenu>
+                      <DropdownMenu.Trigger
+                        as={IconButton}
+                        icon="dot-grid"
+                        variant="ghost"
+                        class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
+                        onClick={(e: MouseEvent) => e.stopPropagation()}
+                        onPointerDown={(e: PointerEvent) => e.stopPropagation()}
+                      />
+                      <DropdownMenu.Portal>
+                        <DropdownMenu.Content class="mt-1">
+                          <DropdownMenu.Item
+                            onSelect={() => {
                               setStore("editServer", {
                                 id: i,
                                 value: i,
@@ -453,54 +503,42 @@ export function DialogSelectServer() {
                               })
                             }}
                           >
-                            {language.t("dialog.server.menu.edit")}
-                          </Button>
+                            <DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
+                          </DropdownMenu.Item>
                           <Show when={isDesktop && defaultUrl() !== i}>
-                            <Button
-                              variant="ghost"
-                              size="normal"
-                              class="justify-start text-md"
-                              onClick={async (e: MouseEvent) => {
-                                e.stopPropagation()
-                                setPopoverOpen(false)
+                            <DropdownMenu.Item
+                              onSelect={async () => {
                                 await platform.setDefaultServerUrl?.(i)
-                                defaultUrlActions.refetch(i)
+                                defaultUrlActions.mutate(i)
                               }}
                             >
-                              {language.t("dialog.server.menu.default")}
-                            </Button>
+                              <DropdownMenu.ItemLabel>
+                                {language.t("dialog.server.menu.default")}
+                              </DropdownMenu.ItemLabel>
+                            </DropdownMenu.Item>
                           </Show>
                           <Show when={isDesktop && defaultUrl() === i}>
-                            <Button
-                              variant="ghost"
-                              size="normal"
-                              class="justify-start text-md"
-                              onClick={async (e: MouseEvent) => {
-                                e.stopPropagation()
-                                setPopoverOpen(false)
+                            <DropdownMenu.Item
+                              onSelect={async () => {
                                 await platform.setDefaultServerUrl?.(null)
-                                defaultUrlActions.refetch(null)
+                                defaultUrlActions.mutate(null)
                               }}
                             >
-                              {language.t("dialog.server.menu.defaultRemove")}
-                            </Button>
+                              <DropdownMenu.ItemLabel>
+                                {language.t("dialog.server.menu.defaultRemove")}
+                              </DropdownMenu.ItemLabel>
+                            </DropdownMenu.Item>
                           </Show>
-                          <div class="h-px bg-border-weak-base my-1" />
-                          <Button
-                            variant="ghost"
-                            size="normal"
-                            class="justify-start text-md text-text-on-critical-base hover:bg-surface-critical-weak"
-                            onClick={(e: MouseEvent) => {
-                              e.stopPropagation()
-                              setPopoverOpen(false)
-                              handleRemove(i)
-                            }}
+                          <DropdownMenu.Separator />
+                          <DropdownMenu.Item
+                            onSelect={() => handleRemove(i)}
+                            class="text-text-on-critical-base hover:bg-surface-critical-weak"
                           >
-                            {language.t("dialog.server.menu.delete")}
-                          </Button>
-                        </div>
-                      </Popover>
-                    </div>
+                            <DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
+                          </DropdownMenu.Item>
+                        </DropdownMenu.Content>
+                      </DropdownMenu.Portal>
+                    </DropdownMenu>
                   </div>
                 </Show>
               </div>
@@ -508,7 +546,7 @@ export function DialogSelectServer() {
           }}
         </List>
 
-        <div class="px-6">
+        <div class="px-5 pb-5">
           <Button
             variant="secondary"
             icon="plus-small"
@@ -517,7 +555,7 @@ export function DialogSelectServer() {
               setStore("addServer", { showForm: true, url: "", error: "" })
               scrollListToBottom()
             }}
-            class="px-3 py-4"
+            class="py-1.5 pl-1.5 pr-3 flex items-center gap-1.5"
           >
             {store.addServer.adding ? language.t("dialog.server.add.checking") : language.t("dialog.server.add.button")}
           </Button>

+ 12 - 4
packages/app/src/components/prompt-input.tsx

@@ -184,6 +184,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     if (!item.commentID) return
 
     comments.setFocus({ file: item.path, id: item.commentID })
+    comments.setActive({ file: item.path, id: item.commentID })
     view().reviewPanel.open()
 
     if (item.commentOrigin === "review") {
@@ -1711,15 +1712,19 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
           <div class="flex flex-nowrap items-start gap-2 p-2 overflow-x-auto no-scrollbar">
             <For each={prompt.context.items()}>
               {(item) => {
+                const active = () => {
+                  const a = comments.active()
+                  return !!item.commentID && item.commentID === a?.id && item.path === a?.file
+                }
                 return (
                   <Tooltip
                     value={
                       <span class="flex max-w-[300px]">
                         <span
                           class="text-text-invert-base truncate min-w-0"
-                          style={{ direction: "rtl", "text-align": "left" }}
+                          style={{ direction: "rtl", "text-align": "left", "unicode-bidi": "plaintext" }}
                         >
-                          <bdi>{getDirectory(item.path)}</bdi>
+                          {getDirectory(item.path)}
                         </span>
                         <span class="shrink-0">{getFilename(item.path)}</span>
                       </span>
@@ -1729,8 +1734,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                   >
                     <div
                       classList={{
-                        "group shrink-0 flex flex-col rounded-[6px] bg-background-stronger pl-2 pr-1 py-1 max-w-[200px] h-12 transition-all shadow-xs-border hover:shadow-xs-border-hover": true,
-                        "cursor-pointer hover:bg-surface-interactive-weak": !!item.commentID,
+                        "group shrink-0 flex flex-col rounded-[6px] pl-2 pr-1 py-1 max-w-[200px] h-12 transition-all transition-transform shadow-xs-border hover:shadow-xs-border-hover": true,
+                        "cursor-pointer hover:bg-surface-interactive-weak": !!item.commentID && !active(),
+                        "cursor-pointer bg-surface-interactive-hover hover:bg-surface-interactive-hover shadow-xs-border-hover":
+                          active(),
+                        "bg-background-stronger": !active(),
                       }}
                       onClick={() => {
                         openComment(item)

+ 8 - 5
packages/app/src/components/session/session-header.tsx

@@ -163,7 +163,10 @@ export function SessionHeader() {
                         ? language.t("session.share.popover.description.shared")
                         : language.t("session.share.popover.description.unshared")
                     }
-                    gutter={8}
+                    gutter={6}
+                    placement="bottom-end"
+                    shift={-64}
+                    class="rounded-xl [&_[data-slot=popover-close-button]]:hidden"
                     triggerAs={Button}
                     triggerProps={{
                       variant: "secondary",
@@ -192,8 +195,8 @@ export function SessionHeader() {
                           </div>
                         }
                       >
-                        <div class="flex flex-col gap-2 w-72">
-                          <TextField value={shareUrl() ?? ""} readOnly copyable class="w-full" />
+                        <div class="flex flex-col gap-2">
+                          <TextField value={shareUrl() ?? ""} readOnly copyable tabIndex={-1} class="w-full" />
                           <div class="grid grid-cols-2 gap-2">
                             <Button
                               size="large"
@@ -231,7 +234,7 @@ export function SessionHeader() {
                       gutter={8}
                     >
                       <IconButton
-                        icon={state.copied ? "check" : "copy"}
+                        icon={state.copied ? "check" : "link"}
                         variant="secondary"
                         class="rounded-l-none"
                         onClick={copyLink}
@@ -246,7 +249,7 @@ export function SessionHeader() {
                   </Show>
                 </div>
               </Show>
-              <div class="hidden md:block shrink-0">
+              <div class="hidden md:flex items-center gap-3 ml-2 shrink-0">
                 <TooltipKeybind
                   title={language.t("command.terminal.toggle")}
                   keybind={command.keybind("terminal.toggle")}

+ 1 - 13
packages/app/src/components/session/session-new-view.tsx

@@ -4,7 +4,6 @@ import { useSync } from "@/context/sync"
 import { useLanguage } from "@/context/language"
 import { Icon } from "@opencode-ai/ui/icon"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
-import { Select } from "@opencode-ai/ui/select"
 
 const MAIN_WORKTREE = "main"
 const CREATE_WORKTREE = "create"
@@ -60,18 +59,7 @@ export function NewSessionView(props: NewSessionViewProps) {
       </div>
       <div class="flex justify-center items-center gap-1">
         <Icon name="branch" size="small" />
-        <Select
-          options={options()}
-          current={current()}
-          value={(x) => x}
-          label={label}
-          onSelect={(value) => {
-            props.onWorktreeChange(value ?? MAIN_WORKTREE)
-          }}
-          size="normal"
-          variant="ghost"
-          class="text-12-medium"
-        />
+        <div class="text-12-medium text-text-weak select-text ml-2">{label(current())}</div>
       </div>
       <Show when={sync.project}>
         {(project) => (

+ 90 - 49
packages/app/src/components/status-popover.tsx

@@ -1,4 +1,4 @@
-import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js"
+import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js"
 import { createStore, reconcile } from "solid-js/store"
 import { useNavigate } from "@solidjs/router"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -7,6 +7,7 @@ import { Tabs } from "@opencode-ai/ui/tabs"
 import { Button } from "@opencode-ai/ui/button"
 import { Switch } from "@opencode-ai/ui/switch"
 import { Icon } from "@opencode-ai/ui/icon"
+import { Tooltip } from "@opencode-ai/ui/tooltip"
 import { useSync } from "@/context/sync"
 import { useSDK } from "@/context/sdk"
 import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/server"
@@ -138,7 +139,8 @@ export function StatusPopover() {
       triggerAs={Button}
       triggerProps={{
         variant: "ghost",
-        class: "rounded-sm w-[75px] h-[24px] py-1.5 pr-3 pl-2 gap-2 border-none shadow-none",
+        class:
+          "rounded-sm w-[75px] h-[24px] py-1.5 pr-3 pl-2 gap-2 border-none shadow-none data-[expanded]:bg-surface-raised-base-active",
         style: { scale: 1 },
       }}
       trigger={
@@ -154,10 +156,15 @@ export function StatusPopover() {
           <span class="text-12-regular text-text-strong">Status</span>
         </div>
       }
-      class="[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] mx-5 bg-transparent border-0 shadow-none rounded-xl"
-      gutter={8}
+      class="[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl"
+      gutter={6}
+      placement="bottom-end"
+      shift={-136}
     >
-      <div class="flex items-center gap-1 w-[360px] border border-border-weak-base rounded-xl">
+      <div
+        class="flex items-center gap-1 w-[360px] rounded-xl"
+        style={{ "box-shadow": "var(--shadow-lg-border-base)" }}
+      >
         <Tabs
           aria-label="Server Configurations"
           class="tabs"
@@ -197,58 +204,92 @@ export function StatusPopover() {
 
           <Tabs.Content value="servers">
             <div class="flex flex-col px-2 pb-2">
-              <div class="flex flex-col p-2 bg-background-base">
+              <div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
                 <For each={sortedServers()}>
                   {(url) => {
                     const isActive = () => url === server.url
                     const isDefault = () => url === defaultServerUrl()
                     const status = () => store.status[url]
                     const isBlocked = () => status()?.healthy === false
+                    const [truncated, setTruncated] = createSignal(false)
+                    let nameRef: HTMLSpanElement | undefined
+                    let versionRef: HTMLSpanElement | undefined
+
+                    onMount(() => {
+                      const check = () => {
+                        const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false
+                        const versionTruncated = versionRef ? versionRef.scrollWidth > versionRef.clientWidth : false
+                        setTruncated(nameTruncated || versionTruncated)
+                      }
+                      check()
+                      window.addEventListener("resize", check)
+                      onCleanup(() => window.removeEventListener("resize", check))
+                    })
+
+                    const tooltipValue = () => {
+                      const name = serverDisplayName(url)
+                      const version = status()?.version
+                      return (
+                        <span class="flex items-center gap-2">
+                          <span>{name}</span>
+                          <Show when={version}>
+                            <span class="text-text-invert-base">{version}</span>
+                          </Show>
+                        </span>
+                      )
+                    }
+
                     return (
-                      <button
-                        type="button"
-                        class="flex items-center gap-2 w-full px-2 py-1 rounded-md transition-colors text-left"
-                        classList={{
-                          "opacity-50": isBlocked(),
-                          "hover:bg-surface-raised-base-hover": !isBlocked(),
-                          "cursor-not-allowed": isBlocked(),
-                        }}
-                        aria-disabled={isBlocked()}
-                        onClick={() => {
-                          if (isBlocked()) return
-                          server.setActive(url)
-                          navigate("/")
-                        }}
-                      >
-                        <div
+                      <Tooltip value={tooltipValue()} placement="top" inactive={!truncated()}>
+                        <button
+                          type="button"
+                          class="flex items-center gap-2 w-full h-8 pl-3 pr-1.5 py-1.5 rounded-md transition-colors text-left"
                           classList={{
-                            "size-1.5 rounded-full shrink-0": true,
-                            "bg-icon-success-base": status()?.healthy === true,
-                            "bg-icon-critical-base": status()?.healthy === false,
-                            "bg-border-weak-base": status() === undefined,
+                            "opacity-50": isBlocked(),
+                            "hover:bg-surface-raised-base-hover": !isBlocked(),
+                            "cursor-not-allowed": isBlocked(),
                           }}
-                        />
-                        <span class="text-14-regular text-text-base truncate">{serverDisplayName(url)}</span>
-                        <Show when={status()?.version}>
-                          <span class="text-12-regular text-text-weak">{status()?.version}</span>
-                        </Show>
-                        <Show when={isDefault()}>
-                          <span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
-                            Default
+                          aria-disabled={isBlocked()}
+                          onClick={() => {
+                            if (isBlocked()) return
+                            server.setActive(url)
+                            navigate("/")
+                          }}
+                        >
+                          <div
+                            classList={{
+                              "size-1.5 rounded-full shrink-0": true,
+                              "bg-icon-success-base": status()?.healthy === true,
+                              "bg-icon-critical-base": status()?.healthy === false,
+                              "bg-border-weak-base": status() === undefined,
+                            }}
+                          />
+                          <span ref={nameRef} class="text-14-regular text-text-base truncate">
+                            {serverDisplayName(url)}
                           </span>
-                        </Show>
-                        <div class="flex-1" />
-                        <Show when={isActive()}>
-                          <Icon name="check" size="small" class="text-icon-weak shrink-0" />
-                        </Show>
-                      </button>
+                          <Show when={status()?.version}>
+                            <span ref={versionRef} class="text-12-regular text-text-weak truncate">
+                              {status()?.version}
+                            </span>
+                          </Show>
+                          <Show when={isDefault()}>
+                            <span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
+                              Default
+                            </span>
+                          </Show>
+                          <div class="flex-1" />
+                          <Show when={isActive()}>
+                            <Icon name="check" size="small" class="text-icon-weak shrink-0" />
+                          </Show>
+                        </button>
+                      </Tooltip>
                     )
                   }}
                 </For>
 
                 <Button
                   variant="secondary"
-                  class="mt-2 self-start"
+                  class="mt-3 self-start h-8 px-3 py-1.5"
                   onClick={() => dialog.show(() => <DialogSelectServer />)}
                 >
                   Manage servers
@@ -259,11 +300,11 @@ export function StatusPopover() {
 
           <Tabs.Content value="mcp">
             <div class="flex flex-col px-2 pb-2">
-              <div class="flex flex-col p-2 bg-background-base">
+              <div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
                 <Show
                   when={mcpItems().length > 0}
                   fallback={
-                    <div class="text-14-regular text-text-weak text-center py-4">No MCP servers configured</div>
+                    <div class="text-14-regular text-text-base text-center my-auto">No MCP servers configured</div>
                   }
                 >
                   <For each={mcpItems()}>
@@ -272,7 +313,7 @@ export function StatusPopover() {
                       return (
                         <button
                           type="button"
-                          class="flex items-center gap-2 w-full px-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
+                          class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
                           onClick={() => toggleMcp(item.name)}
                           disabled={loading() === item.name}
                         >
@@ -305,11 +346,11 @@ export function StatusPopover() {
 
           <Tabs.Content value="lsp">
             <div class="flex flex-col px-2 pb-2">
-              <div class="flex flex-col p-2 bg-background-base">
+              <div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
                 <Show
                   when={lspItems().length > 0}
                   fallback={
-                    <div class="text-14-regular text-text-weak text-center py-4">
+                    <div class="text-14-regular text-text-base text-center my-auto">
                       LSPs auto-detected from file types
                     </div>
                   }
@@ -335,13 +376,13 @@ export function StatusPopover() {
 
           <Tabs.Content value="plugins">
             <div class="flex flex-col px-2 pb-2">
-              <div class="flex flex-col p-2 bg-background-base">
+              <div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
                 <Show
                   when={plugins().length > 0}
                   fallback={
-                    <div class="text-14-regular text-text-weak text-center py-4">
+                    <div class="text-14-regular text-text-base text-center my-auto">
                       Plugins configured in{" "}
-                      <code class="bg-surface-raised-base px-1.5 py-0.5 rounded-sm">opencode.json</code>
+                      <code class="bg-surface-raised-base px-1.5 py-0.5 rounded-sm text-text-base">opencode.json</code>
                     </div>
                   }
                 >

+ 6 - 0
packages/app/src/components/terminal.tsx

@@ -111,6 +111,8 @@ export const Terminal = (props: TerminalProps) => {
     const mod = await import("ghostty-web")
     ghostty = await mod.Ghostty.load()
 
+    const once = { value: false }
+
     const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
     if (window.__OPENCODE__?.serverPassword) {
       url.username = "opencode"
@@ -258,6 +260,8 @@ export const Terminal = (props: TerminalProps) => {
     })
     socket.addEventListener("error", (error) => {
       if (disposed) return
+      if (once.value) return
+      once.value = true
       console.error("WebSocket error:", error)
       local.onConnectError?.(error)
     })
@@ -266,6 +270,8 @@ export const Terminal = (props: TerminalProps) => {
       // Normal closure (code 1000) means PTY process exited - server event handles cleanup
       // For other codes (network issues, server restart), trigger error handler
       if (event.code !== 1000) {
+        if (once.value) return
+        once.value = true
         local.onConnectError?.(new Error(`WebSocket closed abnormally: ${event.code}`))
       }
     })

+ 2 - 1
packages/app/src/components/titlebar.tsx

@@ -81,7 +81,7 @@ export function Titlebar() {
         classList={{
           "flex items-center w-full min-w-0": true,
           "pl-2": !mac(),
-          "pr-2": !windows(),
+          "pr-6": !windows(),
         }}
         onMouseDown={drag}
         data-tauri-drag-region
@@ -145,6 +145,7 @@ export function Titlebar() {
           data-tauri-drag-region
         />
         <Show when={windows()}>
+          <div class="w-6 shrink-0" />
           <div data-tauri-decorum-tb class="flex flex-row" />
         </Show>
       </div>

+ 7 - 0
packages/app/src/context/comments.tsx

@@ -38,6 +38,7 @@ function createCommentSession(dir: string, id: string | undefined) {
   )
 
   const [focus, setFocus] = createSignal<CommentFocus | null>(null)
+  const [active, setActive] = createSignal<CommentFocus | null>(null)
 
   const list = (file: string) => store.comments[file] ?? []
 
@@ -76,6 +77,9 @@ function createCommentSession(dir: string, id: string | undefined) {
     focus: createMemo(() => focus()),
     setFocus,
     clearFocus: () => setFocus(null),
+    active: createMemo(() => active()),
+    setActive,
+    clearActive: () => setActive(null),
   }
 }
 
@@ -135,6 +139,9 @@ export const { use: useComments, provider: CommentsProvider } = createSimpleCont
       focus: () => session().focus(),
       setFocus: (focus: CommentFocus | null) => session().setFocus(focus),
       clearFocus: () => session().clearFocus(),
+      active: () => session().active(),
+      setActive: (active: CommentFocus | null) => session().setActive(active),
+      clearActive: () => session().clearActive(),
     }
   },
 })

+ 11 - 7
packages/app/src/context/terminal.tsx

@@ -13,7 +13,6 @@ export type LocalPTY = {
   cols?: number
   buffer?: string
   scrollY?: number
-  error?: boolean
 }
 
 const WORKSPACE_KEY = "__workspace__"
@@ -151,13 +150,18 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, sess
           return undefined
         })
       if (!clone?.data) return
-      setStore("all", index, {
-        ...pty,
-        ...clone.data,
+
+      const active = store.active === pty.id
+
+      batch(() => {
+        setStore("all", index, {
+          ...pty,
+          ...clone.data,
+        })
+        if (active) {
+          setStore("active", clone.data.id)
+        }
       })
-      if (store.active === pty.id) {
-        setStore("active", clone.data.id)
-      }
     },
     open(id: string) {
       setStore("active", id)

+ 33 - 78
packages/app/src/pages/session.tsx

@@ -1,16 +1,4 @@
-import {
-  For,
-  Index,
-  onCleanup,
-  onMount,
-  Show,
-  Match,
-  Switch,
-  createMemo,
-  createEffect,
-  on,
-  createSignal,
-} from "solid-js"
+import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on, createSignal } from "solid-js"
 import { createMediaQuery } from "@solid-primitives/media"
 import { createResizeObserver } from "@solid-primitives/resize-observer"
 import { Dynamic } from "solid-js/web"
@@ -41,7 +29,6 @@ import { useLayout } from "@/context/layout"
 import { Terminal } from "@/components/terminal"
 import { checksum, base64Encode, base64Decode } from "@opencode-ai/util/encode"
 import { findLast } from "@opencode-ai/util/array"
-import { getFilename } from "@opencode-ai/util/path"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { DialogSelectFile } from "@/components/dialog-select-file"
 import { DialogSelectModel } from "@/components/dialog-select-model"
@@ -67,7 +54,6 @@ import {
   SortableTerminalTab,
   NewSessionView,
 } from "@/components/session"
-import { usePlatform } from "@/context/platform"
 import { navMark, navParams } from "@/utils/perf"
 import { same } from "@/utils/same"
 
@@ -103,7 +89,7 @@ function SessionReviewTab(props: SessionReviewTabProps) {
 
   const sdk = useSDK()
 
-  const readFile = (path: string) => {
+  const readFile = async (path: string) => {
     return sdk.client.file
       .read({ path })
       .then((x) => x.data)
@@ -192,7 +178,6 @@ export default function Page() {
   const codeComponent = useCodeComponent()
   const command = useCommand()
   const language = useLanguage()
-  const platform = usePlatform()
   const params = useParams()
   const navigate = useNavigate()
   const sdk = useSDK()
@@ -1537,9 +1522,9 @@ export default function Page() {
                         }}
                         onClick={autoScroll.handleInteraction}
                         class="relative min-w-0 w-full h-full overflow-y-auto session-scroller"
-                        style={{ "--session-title-height": info()?.title ? "40px" : "0px" }}
+                        style={{ "--session-title-height": info()?.title || info()?.parentID ? "40px" : "0px" }}
                       >
-                        <Show when={info()?.title}>
+                        <Show when={info()?.title || info()?.parentID}>
                           <div
                             classList={{
                               "sticky top-0 z-30 bg-background-stronger": true,
@@ -1548,8 +1533,21 @@ export default function Page() {
                               "md:max-w-200 md:mx-auto": !showTabs(),
                             }}
                           >
-                            <div class="h-10 flex items-center">
-                              <h1 class="text-16-medium text-text-strong truncate">{info()?.title}</h1>
+                            <div class="h-10 flex items-center gap-1">
+                              <Show when={info()?.parentID}>
+                                <IconButton
+                                  tabIndex={-1}
+                                  icon="arrow-left"
+                                  variant="ghost"
+                                  onClick={() => {
+                                    navigate(`/${params.dir}/session/${info()?.parentID}`)
+                                  }}
+                                  aria-label={language.t("common.goBack")}
+                                />
+                              </Show>
+                              <Show when={info()?.title}>
+                                <h1 class="text-16-medium text-text-strong truncate">{info()?.title}</h1>
+                              </Show>
                             </div>
                           </div>
                         </Show>
@@ -2453,66 +2451,23 @@ export default function Page() {
                 </Tabs>
                 <div class="flex-1 min-h-0 relative">
                   <For each={terminal.all()}>
-                    {(pty) => {
-                      const [dismissed, setDismissed] = createSignal(false)
-                      return (
-                        <div
-                          id={`terminal-wrapper-${pty.id}`}
-                          class="absolute inset-0"
-                          style={{
-                            display: terminal.active() === pty.id ? "block" : "none",
-                          }}
-                        >
+                    {(pty) => (
+                      <div
+                        id={`terminal-wrapper-${pty.id}`}
+                        class="absolute inset-0"
+                        style={{
+                          display: terminal.active() === pty.id ? "block" : "none",
+                        }}
+                      >
+                        <Show when={pty.id} keyed>
                           <Terminal
                             pty={pty}
-                            onCleanup={(data) => terminal.update({ ...data, id: pty.id })}
-                            onConnect={() => {
-                              terminal.update({ id: pty.id, error: false })
-                              setDismissed(false)
-                            }}
-                            onConnectError={() => {
-                              setDismissed(false)
-                              terminal.update({ id: pty.id, error: true })
-                            }}
+                            onCleanup={terminal.update}
+                            onConnectError={() => terminal.clone(pty.id)}
                           />
-                          <Show when={pty.error && !dismissed()}>
-                            <div
-                              class="absolute inset-0 flex flex-col items-center justify-center gap-3"
-                              style={{ "background-color": "rgba(0, 0, 0, 0.6)" }}
-                            >
-                              <Icon
-                                name="circle-ban-sign"
-                                class="w-8 h-8"
-                                style={{ color: "rgba(239, 68, 68, 0.8)" }}
-                              />
-                              <div class="text-center" style={{ color: "rgba(255, 255, 255, 0.7)" }}>
-                                <div class="text-14-semibold mb-1">{language.t("terminal.connectionLost.title")}</div>
-                                <div class="text-12-regular" style={{ color: "rgba(255, 255, 255, 0.5)" }}>
-                                  {language.t("terminal.connectionLost.description")}
-                                </div>
-                              </div>
-                              <button
-                                class="mt-2 px-3 py-1.5 text-12-medium rounded-lg transition-colors"
-                                style={{
-                                  "background-color": "rgba(255, 255, 255, 0.1)",
-                                  color: "rgba(255, 255, 255, 0.7)",
-                                  border: "1px solid rgba(255, 255, 255, 0.2)",
-                                }}
-                                onMouseEnter={(e) =>
-                                  (e.currentTarget.style.backgroundColor = "rgba(255, 255, 255, 0.15)")
-                                }
-                                onMouseLeave={(e) =>
-                                  (e.currentTarget.style.backgroundColor = "rgba(255, 255, 255, 0.1)")
-                                }
-                                onClick={() => setDismissed(true)}
-                              >
-                                {language.t("common.dismiss")}
-                              </button>
-                            </div>
-                          </Show>
-                        </div>
-                      )
-                    }}
+                        </Show>
+                      </div>
+                    )}
                   </For>
                 </div>
               </div>

+ 1 - 1
packages/console/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/console-app",
-  "version": "1.1.34",
+  "version": "1.1.35",
   "type": "module",
   "license": "MIT",
   "scripts": {

+ 1 - 1
packages/console/core/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@opencode-ai/console-core",
-  "version": "1.1.34",
+  "version": "1.1.35",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/console/function/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/console-function",
-  "version": "1.1.34",
+  "version": "1.1.35",
   "$schema": "https://json.schemastore.org/package.json",
   "private": true,
   "type": "module",

+ 1 - 1
packages/console/mail/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/console-mail",
-  "version": "1.1.34",
+  "version": "1.1.35",
   "dependencies": {
     "@jsx-email/all": "2.2.3",
     "@jsx-email/cli": "1.4.3",

+ 1 - 1
packages/desktop/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@opencode-ai/desktop",
   "private": true,
-  "version": "1.1.34",
+  "version": "1.1.35",
   "type": "module",
   "license": "MIT",
   "scripts": {

+ 1 - 1
packages/enterprise/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/enterprise",
-  "version": "1.1.34",
+  "version": "1.1.35",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 6 - 6
packages/extensions/zed/extension.toml

@@ -1,7 +1,7 @@
 id = "opencode"
 name = "OpenCode"
 description = "The open source coding agent."
-version = "1.1.34"
+version = "1.1.35"
 schema_version = 1
 authors = ["Anomaly"]
 repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
 icon = "./icons/opencode.svg"
 
 [agent_servers.opencode.targets.darwin-aarch64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.34/opencode-darwin-arm64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.35/opencode-darwin-arm64.zip"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.darwin-x86_64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.34/opencode-darwin-x64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.35/opencode-darwin-x64.zip"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.linux-aarch64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.34/opencode-linux-arm64.tar.gz"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.35/opencode-linux-arm64.tar.gz"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.linux-x86_64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.34/opencode-linux-x64.tar.gz"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.35/opencode-linux-x64.tar.gz"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.windows-x86_64]
-archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.34/opencode-windows-x64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.35/opencode-windows-x64.zip"
 cmd = "./opencode.exe"
 args = ["acp"]

+ 1 - 1
packages/function/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/function",
-  "version": "1.1.34",
+  "version": "1.1.35",
   "$schema": "https://json.schemastore.org/package.json",
   "private": true,
   "type": "module",

+ 1 - 1
packages/opencode/package.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "1.1.34",
+  "version": "1.1.35",
   "name": "opencode",
   "type": "module",
   "license": "MIT",

+ 2 - 2
packages/opencode/src/question/index.ts

@@ -10,7 +10,7 @@ export namespace Question {
 
   export const Option = z
     .object({
-      label: z.string().max(30).describe("Display text (1-5 words, concise)"),
+      label: z.string().describe("Display text (1-5 words, concise)"),
       description: z.string().describe("Explanation of choice"),
     })
     .meta({
@@ -21,7 +21,7 @@ export namespace Question {
   export const Info = z
     .object({
       question: z.string().describe("Complete question"),
-      header: z.string().max(30).describe("Very short label (max 30 chars)"),
+      header: z.string().describe("Very short label (max 30 chars)"),
       options: z.array(Option).describe("Available choices"),
       multiple: z.boolean().optional().describe("Allow selecting multiple choices"),
       custom: z.boolean().optional().describe("Allow typing a custom answer (default: true)"),

+ 1 - 1
packages/plugin/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@opencode-ai/plugin",
-  "version": "1.1.34",
+  "version": "1.1.35",
   "type": "module",
   "license": "MIT",
   "scripts": {

+ 2 - 2
packages/sdk/js/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@opencode-ai/sdk",
-  "version": "1.1.34",
+  "version": "1.1.35",
   "type": "module",
   "license": "MIT",
   "scripts": {
@@ -20,7 +20,7 @@
     "dist"
   ],
   "devDependencies": {
-    "@hey-api/openapi-ts": "0.90.4",
+    "@hey-api/openapi-ts": "0.90.10",
     "@tsconfig/node22": "catalog:",
     "@types/node": "catalog:",
     "typescript": "catalog:",

+ 22 - 2
packages/sdk/js/src/v2/gen/sdk.gen.ts

@@ -2518,7 +2518,17 @@ export class Control extends HeyApiClient {
     },
     options?: Options<never, ThrowOnError>,
   ) {
-    const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }, { in: "body" }] }])
+    const params = buildClientParams(
+      [parameters],
+      [
+        {
+          args: [
+            { in: "query", key: "directory" },
+            { key: "body", map: "body" },
+          ],
+        },
+      ],
+    )
     return (options?.client ?? this.client).post<TuiControlResponseResponses, unknown, ThrowOnError>({
       url: "/tui/control/response",
       ...options,
@@ -2770,7 +2780,17 @@ export class Tui extends HeyApiClient {
     },
     options?: Options<never, ThrowOnError>,
   ) {
-    const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }, { in: "body" }] }])
+    const params = buildClientParams(
+      [parameters],
+      [
+        {
+          args: [
+            { in: "query", key: "directory" },
+            { key: "body", map: "body" },
+          ],
+        },
+      ],
+    )
     return (options?.client ?? this.client).post<TuiPublishResponses, TuiPublishErrors, ThrowOnError>({
       url: "/tui/publish",
       ...options,

+ 2 - 4
packages/sdk/openapi.json

@@ -7319,8 +7319,7 @@
         "properties": {
           "label": {
             "description": "Display text (1-5 words, concise)",
-            "type": "string",
-            "maxLength": 30
+            "type": "string"
           },
           "description": {
             "description": "Explanation of choice",
@@ -7338,8 +7337,7 @@
           },
           "header": {
             "description": "Very short label (max 30 chars)",
-            "type": "string",
-            "maxLength": 30
+            "type": "string"
           },
           "options": {
             "description": "Available choices",

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/slack",
-  "version": "1.1.34",
+  "version": "1.1.35",
   "type": "module",
   "license": "MIT",
   "scripts": {

+ 2 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/ui",
-  "version": "1.1.34",
+  "version": "1.1.35",
   "type": "module",
   "license": "MIT",
   "exports": {
@@ -56,6 +56,7 @@
     "marked": "catalog:",
     "marked-katex-extension": "5.1.6",
     "marked-shiki": "catalog:",
+    "morphdom": "2.7.8",
     "remeda": "catalog:",
     "shiki": "catalog:",
     "solid-js": "catalog:",

+ 1 - 1
packages/ui/src/components/dialog.css

@@ -66,7 +66,7 @@
 
       [data-slot="dialog-header"] {
         display: flex;
-        padding: 16px 16px 16px 24px;
+        padding: 20px;
         justify-content: space-between;
         align-items: center;
         flex-shrink: 0;

+ 1 - 0
packages/ui/src/components/icon.tsx

@@ -72,6 +72,7 @@ const icons = {
   keyboard: `<path d="M5.125 7.375V4.375H14.875V2.875M8.3125 13.9375H11.6875M8.125 13.9375H11.875M2.125 7.375H17.875V17.125H2.125V7.375ZM5.5 10.375H5.125V10.75H5.5V10.375ZM8.5 10.375H8.125V10.75H8.5V10.375ZM11.875 10.375H11.5V10.75H11.875V10.375ZM14.875 10.375H14.5V10.75H14.875V10.375ZM14.875 13.75H14.5V14.125H14.875V13.75ZM5.5 13.75H5.125V14.125H5.5V13.75Z" stroke="currentColor" stroke-linecap="square"/>`,
   selector: `<path d="M6.66626 12.5033L9.99959 15.8366L13.3329 12.5033M6.66626 7.50326L9.99959 4.16992L13.3329 7.50326" stroke="currentColor" stroke-linecap="square"/>`,
   "arrow-down-to-line": `<path d="M15.2083 11.6667L10 16.875L4.79167 11.6667M10 16.25V3.125" stroke="currentColor" stroke-width="1.25" stroke-linecap="square"/>`,
+  link: `<path d="M2.08334 12.0833L1.72979 11.7298L1.37624 12.0833L1.72979 12.4369L2.08334 12.0833ZM7.91668 17.9167L7.56312 18.2702L7.91668 18.6238L8.27023 18.2702L7.91668 17.9167ZM17.9167 7.91666L18.2702 8.27022L18.6238 7.91666L18.2702 7.56311L17.9167 7.91666ZM12.0833 2.08333L12.4369 1.72977L12.0833 1.37622L11.7298 1.72977L12.0833 2.08333ZM8.39646 5.06311L8.0429 5.41666L8.75001 6.12377L9.10356 5.77021L8.75001 5.41666L8.39646 5.06311ZM5.77023 9.10355L6.12378 8.74999L5.41668 8.04289L5.06312 8.39644L5.41668 8.74999L5.77023 9.10355ZM14.2298 10.8964L13.8762 11.25L14.5833 11.9571L14.9369 11.6035L14.5833 11.25L14.2298 10.8964ZM11.6036 14.9369L11.9571 14.5833L11.25 13.8762L10.8965 14.2298L11.25 14.5833L11.6036 14.9369ZM7.14646 12.1464L6.7929 12.5L7.50001 13.2071L7.85356 12.8535L7.50001 12.5L7.14646 12.1464ZM12.8536 7.85355L13.2071 7.49999L12.5 6.79289L12.1465 7.14644L12.5 7.49999L12.8536 7.85355ZM2.08334 12.0833L1.72979 12.4369L7.56312 18.2702L7.91668 17.9167L8.27023 17.5631L2.4369 11.7298L2.08334 12.0833ZM17.9167 7.91666L18.2702 7.56311L12.4369 1.72977L12.0833 2.08333L11.7298 2.43688L17.5631 8.27022L17.9167 7.91666ZM12.0833 2.08333L11.7298 1.72977L8.39646 5.06311L8.75001 5.41666L9.10356 5.77021L12.4369 2.43688L12.0833 2.08333ZM5.41668 8.74999L5.06312 8.39644L1.72979 11.7298L2.08334 12.0833L2.4369 12.4369L5.77023 9.10355L5.41668 8.74999ZM14.5833 11.25L14.9369 11.6035L18.2702 8.27022L17.9167 7.91666L17.5631 7.56311L14.2298 10.8964L14.5833 11.25ZM7.91668 17.9167L8.27023 18.2702L11.6036 14.9369L11.25 14.5833L10.8965 14.2298L7.56312 17.5631L7.91668 17.9167ZM7.50001 12.5L7.85356 12.8535L12.8536 7.85355L12.5 7.49999L12.1465 7.14644L7.14646 12.1464L7.50001 12.5Z" fill="currentColor"/>`,
 }
 
 export interface IconProps extends ComponentProps<"svg"> {

+ 4 - 0
packages/ui/src/components/line-comment.css

@@ -4,6 +4,10 @@
   z-index: var(--line-comment-z, 30);
 }
 
+[data-component="line-comment"][data-open] {
+  z-index: var(--line-comment-open-z, 100);
+}
+
 [data-component="line-comment"] [data-slot="line-comment-button"] {
   width: 20px;
   height: 20px;

+ 1 - 0
packages/ui/src/components/line-comment.tsx

@@ -25,6 +25,7 @@ export const LineCommentAnchor = (props: LineCommentAnchorProps) => {
       data-component="line-comment"
       data-variant={variant()}
       data-comment-id={props.id}
+      data-open={props.open ? "" : undefined}
       classList={{
         [props.class ?? ""]: !!props.class,
       }}

+ 0 - 1
packages/ui/src/components/list.css

@@ -30,7 +30,6 @@
     gap: 8px;
     align-self: stretch;
     margin-bottom: 4px;
-    padding-right: 4px;
 
     > [data-component="icon-button"] {
       width: 24px;

+ 2 - 0
packages/ui/src/components/list.tsx

@@ -38,6 +38,7 @@ export interface ListProps<T> extends FilteredListProps<T> {
   loadingMessage?: string
   onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
   onMove?: (item: T | undefined) => void
+  onFilter?: (value: string) => void
   activeIcon?: IconProps["name"]
   filter?: string
   search?: ListSearchProps | boolean
@@ -98,6 +99,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
     const current = internalFilter()
     if (prev !== current) {
       onInput(current)
+      props.onFilter?.(current)
     }
     return current
   }, "")

+ 49 - 6
packages/ui/src/components/markdown.tsx

@@ -1,6 +1,7 @@
 import { useMarked } from "../context/marked"
 import { useI18n } from "../context/i18n"
 import DOMPurify from "dompurify"
+import morphdom from "morphdom"
 import { checksum } from "@opencode-ai/util/encode"
 import { ComponentProps, createEffect, createResource, createSignal, onCleanup, splitProps } from "solid-js"
 import { isServer } from "solid-js/web"
@@ -194,18 +195,61 @@ export function Markdown(
     { initialValue: "" },
   )
 
+  let copySetupTimer: ReturnType<typeof setTimeout> | undefined
+  let copyCleanup: (() => void) | undefined
+
   createEffect(() => {
     const container = root()
     const content = html()
     if (!container) return
-    if (!content) return
     if (isServer) return
-    const cleanup = setupCodeCopy(container, {
-      copy: i18n.t("ui.message.copy"),
-      copied: i18n.t("ui.message.copied"),
+
+    if (!content) {
+      container.innerHTML = ""
+      return
+    }
+
+    const temp = document.createElement("div")
+    temp.innerHTML = content
+
+    morphdom(container, temp, {
+      childrenOnly: true,
+      onBeforeElUpdated: (fromEl, toEl) => {
+        if (fromEl.isEqualNode(toEl)) return false
+        if (fromEl.getAttribute("data-component") === "markdown-code") {
+          const fromPre = fromEl.querySelector("pre")
+          const toPre = toEl.querySelector("pre")
+          if (fromPre && toPre && !fromPre.isEqualNode(toPre)) {
+            morphdom(fromPre, toPre)
+          }
+          return false
+        }
+        return true
+      },
+      onBeforeNodeDiscarded: (node) => {
+        if (node instanceof Element) {
+          if (node.getAttribute("data-slot") === "markdown-copy-button") return false
+          if (node.getAttribute("data-component") === "markdown-code") return false
+        }
+        return true
+      },
     })
-    onCleanup(cleanup)
+
+    if (copySetupTimer) clearTimeout(copySetupTimer)
+    copySetupTimer = setTimeout(() => {
+      if (copyCleanup) copyCleanup()
+      copyCleanup = setupCodeCopy(container, {
+        copy: i18n.t("ui.message.copy"),
+        copied: i18n.t("ui.message.copied"),
+      })
+    }, 150)
   })
+
+  onCleanup(() => {
+    if (copySetupTimer) clearTimeout(copySetupTimer)
+    if (copyCleanup) copyCleanup()
+  })
+
   return (
     <div
       data-component="markdown"
@@ -213,7 +257,6 @@ export function Markdown(
         ...(local.classList ?? {}),
         [local.class ?? ""]: !!local.class,
       }}
-      innerHTML={html.latest}
       ref={setRoot}
       {...others}
     />

+ 98 - 3
packages/ui/src/components/popover.tsx

@@ -1,5 +1,15 @@
 import { Popover as Kobalte } from "@kobalte/core/popover"
-import { ComponentProps, JSXElement, ParentProps, Show, splitProps, ValidComponent } from "solid-js"
+import {
+  ComponentProps,
+  JSXElement,
+  ParentProps,
+  Show,
+  createEffect,
+  createSignal,
+  onCleanup,
+  splitProps,
+  ValidComponent,
+} from "solid-js"
 import { useI18n } from "../context/i18n"
 import { IconButton } from "./icon-button"
 
@@ -13,6 +23,7 @@ export interface PopoverProps<T extends ValidComponent = "div">
   description?: JSXElement
   class?: ComponentProps<"div">["class"]
   classList?: ComponentProps<"div">["classList"]
+  style?: ComponentProps<"div">["style"]
   portal?: boolean
 }
 
@@ -26,17 +37,96 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
     "description",
     "class",
     "classList",
+    "style",
     "children",
     "portal",
+    "open",
+    "defaultOpen",
+    "onOpenChange",
+    "modal",
   ])
 
+  const [contentRef, setContentRef] = createSignal<HTMLElement | undefined>(undefined)
+  const [triggerRef, setTriggerRef] = createSignal<HTMLElement | undefined>(undefined)
+  const [dismiss, setDismiss] = createSignal<"escape" | "outside" | null>(null)
+
+  const [uncontrolledOpen, setUncontrolledOpen] = createSignal<boolean>(local.defaultOpen ?? false)
+
+  const controlled = () => local.open !== undefined
+  const opened = () => {
+    if (controlled()) return local.open ?? false
+    return uncontrolledOpen()
+  }
+
+  const onOpenChange = (next: boolean) => {
+    if (next) setDismiss(null)
+    if (local.onOpenChange) local.onOpenChange(next)
+    if (controlled()) return
+    setUncontrolledOpen(next)
+  }
+
+  createEffect(() => {
+    if (!opened()) return
+
+    const inside = (node: Node | null | undefined) => {
+      if (!node) return false
+      const content = contentRef()
+      if (content && content.contains(node)) return true
+      const trigger = triggerRef()
+      if (trigger && trigger.contains(node)) return true
+      return false
+    }
+
+    const close = (reason: "escape" | "outside") => {
+      setDismiss(reason)
+      onOpenChange(false)
+    }
+
+    const onKeyDown = (event: KeyboardEvent) => {
+      if (event.key !== "Escape") return
+      close("escape")
+      event.preventDefault()
+      event.stopPropagation()
+    }
+
+    const onPointerDown = (event: PointerEvent) => {
+      const target = event.target
+      if (!(target instanceof Node)) return
+      if (inside(target)) return
+      close("outside")
+    }
+
+    const onFocusIn = (event: FocusEvent) => {
+      const target = event.target
+      if (!(target instanceof Node)) return
+      if (inside(target)) return
+      close("outside")
+    }
+
+    window.addEventListener("keydown", onKeyDown, true)
+    window.addEventListener("pointerdown", onPointerDown, true)
+    window.addEventListener("focusin", onFocusIn, true)
+
+    onCleanup(() => {
+      window.removeEventListener("keydown", onKeyDown, true)
+      window.removeEventListener("pointerdown", onPointerDown, true)
+      window.removeEventListener("focusin", onFocusIn, true)
+    })
+  })
+
   const content = () => (
     <Kobalte.Content
+      ref={(el: HTMLElement | undefined) => setContentRef(el)}
       data-component="popover-content"
       classList={{
         ...(local.classList ?? {}),
         [local.class ?? ""]: !!local.class,
       }}
+      style={local.style}
+      onCloseAutoFocus={(event: Event) => {
+        if (dismiss() === "outside") event.preventDefault()
+        setDismiss(null)
+      }}
     >
       {/* <Kobalte.Arrow data-slot="popover-arrow" /> */}
       <Show when={local.title}>
@@ -59,8 +149,13 @@ export function Popover<T extends ValidComponent = "div">(props: PopoverProps<T>
   )
 
   return (
-    <Kobalte gutter={4} {...rest}>
-      <Kobalte.Trigger as={local.triggerAs ?? "div"} data-slot="popover-trigger" {...(local.triggerProps as any)}>
+    <Kobalte gutter={4} {...rest} open={opened()} onOpenChange={onOpenChange} modal={local.modal ?? false}>
+      <Kobalte.Trigger
+        ref={(el: HTMLElement) => setTriggerRef(el)}
+        as={local.triggerAs ?? "div"}
+        data-slot="popover-trigger"
+        {...(local.triggerProps as any)}
+      >
         {local.trigger}
       </Kobalte.Trigger>
       <Show when={local.portal ?? true} fallback={content()}>

+ 1 - 1
packages/ui/src/components/session-review.css

@@ -267,6 +267,6 @@
     position: relative;
     overflow: hidden;
     --line-comment-z: 5;
-    --line-comment-popover-z: 6;
+    --line-comment-popover-z: 30;
   }
 }

+ 7 - 1
packages/ui/src/components/tabs.css

@@ -63,6 +63,12 @@
       align-items: center;
       justify-content: center;
       padding: 14px 24px;
+      outline: none;
+
+      &:focus-visible {
+        outline: none;
+        box-shadow: none;
+      }
     }
 
     [data-slot="tabs-trigger-close-button"] {
@@ -81,7 +87,7 @@
     }
     &:focus-visible {
       outline: none;
-      box-shadow: 0 0 0 2px var(--border-focus);
+      box-shadow: none;
     }
     &:has([data-hidden]) {
       [data-slot="tabs-trigger-close-button"] {

+ 1 - 1
packages/ui/src/components/text-field.css

@@ -51,7 +51,7 @@
       border: 1px solid var(--border-weak-base);
       background: var(--input-base);
 
-      &:focus-within {
+      &:focus-within:not(:has([data-readonly])) {
         border-color: transparent;
         /* border/shadow-xs/select */
         box-shadow:

+ 7 - 4
packages/ui/src/components/text-field.tsx

@@ -93,17 +93,20 @@ export function TextField(props: TextFieldProps) {
         </Show>
         <Show when={local.copyable}>
           <Tooltip
-            value={copied() ? i18n.t("ui.textField.copied") : i18n.t("ui.textField.copyToClipboard")}
+            value={copied() ? i18n.t("ui.textField.copied") : i18n.t("ui.textField.copyLink")}
             placement="top"
-            gutter={8}
+            gutter={4}
+            forceOpen={copied()}
+            skipDelayDuration={0}
           >
             <IconButton
               type="button"
-              icon={copied() ? "check" : "copy"}
+              icon={copied() ? "check" : "link"}
               variant="ghost"
               onClick={handleCopy}
+              tabIndex={-1}
               data-slot="input-copy-button"
-              aria-label={copied() ? i18n.t("ui.textField.copied") : i18n.t("ui.textField.copyToClipboard")}
+              aria-label={copied() ? i18n.t("ui.textField.copied") : i18n.t("ui.textField.copyLink")}
             />
           </Tooltip>
         </Show>

+ 1 - 1
packages/ui/src/components/tooltip.css

@@ -44,7 +44,7 @@
     /* transform: translate3d(0, 0, 0); */
   }
 
-  &[data-closed] {
+  &[data-closed]:not([data-force-open="true"]) {
     opacity: 0;
   }
 

+ 11 - 2
packages/ui/src/components/tooltip.tsx

@@ -8,6 +8,7 @@ export interface TooltipProps extends ComponentProps<typeof KobalteTooltip> {
   contentClass?: string
   contentStyle?: JSX.CSSProperties
   inactive?: boolean
+  forceOpen?: boolean
 }
 
 export interface TooltipKeybindProps extends Omit<TooltipProps, "value"> {
@@ -32,7 +33,14 @@ export function TooltipKeybind(props: TooltipKeybindProps) {
 
 export function Tooltip(props: TooltipProps) {
   const [open, setOpen] = createSignal(false)
-  const [local, others] = splitProps(props, ["children", "class", "contentClass", "contentStyle", "inactive"])
+  const [local, others] = splitProps(props, [
+    "children",
+    "class",
+    "contentClass",
+    "contentStyle",
+    "inactive",
+    "forceOpen",
+  ])
 
   const c = children(() => local.children)
 
@@ -55,7 +63,7 @@ export function Tooltip(props: TooltipProps) {
     <Switch>
       <Match when={local.inactive}>{local.children}</Match>
       <Match when={true}>
-        <KobalteTooltip forceMount gutter={4} {...others} open={open()} onOpenChange={setOpen}>
+        <KobalteTooltip forceMount gutter={4} {...others} open={local.forceOpen || open()} onOpenChange={setOpen}>
           <KobalteTooltip.Trigger as={"div"} data-component="tooltip-trigger" class={local.class}>
             {c()}
           </KobalteTooltip.Trigger>
@@ -63,6 +71,7 @@ export function Tooltip(props: TooltipProps) {
             <KobalteTooltip.Content
               data-component="tooltip"
               data-placement={props.placement}
+              data-force-open={local.forceOpen}
               class={local.contentClass}
               style={local.contentStyle}
             >

+ 21 - 1
packages/ui/src/hooks/create-auto-scroll.tsx

@@ -30,6 +30,10 @@ export function createAutoScroll(options: AutoScrollOptions) {
     return el.scrollHeight - el.clientHeight - el.scrollTop
   }
 
+  const canScroll = (el: HTMLElement) => {
+    return el.scrollHeight - el.clientHeight > 1
+  }
+
   // Browsers can dispatch scroll events asynchronously. If new content arrives
   // between us calling `scrollTo()` and the subsequent `scroll` event firing,
   // the handler can see a non-zero `distanceFromBottom` and incorrectly assume
@@ -89,6 +93,12 @@ export function createAutoScroll(options: AutoScrollOptions) {
   }
 
   const stop = () => {
+    const el = scroll
+    if (!el) return
+    if (!canScroll(el)) {
+      if (store.userScrolled) setStore("userScrolled", false)
+      return
+    }
     if (store.userScrolled) return
 
     setStore("userScrolled", true)
@@ -111,6 +121,11 @@ export function createAutoScroll(options: AutoScrollOptions) {
     const el = scroll
     if (!el) return
 
+    if (!canScroll(el)) {
+      if (store.userScrolled) setStore("userScrolled", false)
+      return
+    }
+
     if (distanceFromBottom(el) < threshold()) {
       if (store.userScrolled) setStore("userScrolled", false)
       return
@@ -149,6 +164,11 @@ export function createAutoScroll(options: AutoScrollOptions) {
   createResizeObserver(
     () => store.contentRef,
     () => {
+      const el = scroll
+      if (el && !canScroll(el)) {
+        if (store.userScrolled) setStore("userScrolled", false)
+        return
+      }
       if (!active()) return
       if (store.userScrolled) return
       // ResizeObserver fires after layout, before paint.
@@ -159,7 +179,7 @@ export function createAutoScroll(options: AutoScrollOptions) {
   )
 
   createEffect(
-    on(options.working, (working) => {
+    on(options.working, (working: boolean) => {
       settling = false
       if (settleTimer) clearTimeout(settleTimer)
       settleTimer = undefined

+ 6 - 0
packages/ui/src/hooks/use-filtered-list.tsx

@@ -13,6 +13,7 @@ export interface FilteredListProps<T> {
   sortBy?: (a: T, b: T) => number
   sortGroupsBy?: (a: { category: string; items: T[] }, b: { category: string; items: T[] }) => number
   onSelect?: (value: T | undefined, index: number) => void
+  noInitialSelection?: boolean
 }
 
 export function useFilteredList<T>(props: FilteredListProps<T>) {
@@ -57,6 +58,7 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
   })
 
   function initialActive() {
+    if (props.noInitialSelection) return ""
     if (props.current) return props.key(props.current)
 
     const items = flat()
@@ -71,6 +73,10 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
   })
 
   const reset = () => {
+    if (props.noInitialSelection) {
+      list.setActive("")
+      return
+    }
     const all = flat()
     if (all.length === 0) return
     list.setActive(props.key(all[0]))

+ 1 - 0
packages/ui/src/i18n/ar.ts

@@ -40,6 +40,7 @@ export const dict = {
   "ui.messageNav.newMessage": "رسالة جديدة",
 
   "ui.textField.copyToClipboard": "نسخ إلى الحافظة",
+  "ui.textField.copyLink": "نسخ الرابط",
   "ui.textField.copied": "تم النسخ",
 
   "ui.imagePreview.alt": "معاينة الصورة",

+ 1 - 0
packages/ui/src/i18n/br.ts

@@ -40,6 +40,7 @@ export const dict = {
   "ui.messageNav.newMessage": "Nova mensagem",
 
   "ui.textField.copyToClipboard": "Copiar para área de transferência",
+  "ui.textField.copyLink": "Copiar link",
   "ui.textField.copied": "Copiado",
 
   "ui.imagePreview.alt": "Visualização de imagem",

+ 1 - 0
packages/ui/src/i18n/da.ts

@@ -40,6 +40,7 @@ export const dict = {
   "ui.messageNav.newMessage": "Ny besked",
 
   "ui.textField.copyToClipboard": "Kopier til udklipsholder",
+  "ui.textField.copyLink": "Kopier link",
   "ui.textField.copied": "Kopieret",
 
   "ui.imagePreview.alt": "Billedforhåndsvisning",

+ 1 - 0
packages/ui/src/i18n/de.ts

@@ -44,6 +44,7 @@ export const dict = {
   "ui.messageNav.newMessage": "Neue Nachricht",
 
   "ui.textField.copyToClipboard": "In die Zwischenablage kopieren",
+  "ui.textField.copyLink": "Link kopieren",
   "ui.textField.copied": "Kopiert",
 
   "ui.imagePreview.alt": "Bildvorschau",

+ 1 - 0
packages/ui/src/i18n/en.ts

@@ -40,6 +40,7 @@ export const dict = {
   "ui.messageNav.newMessage": "New message",
 
   "ui.textField.copyToClipboard": "Copy to clipboard",
+  "ui.textField.copyLink": "Copy link",
   "ui.textField.copied": "Copied",
 
   "ui.imagePreview.alt": "Image preview",

+ 1 - 0
packages/ui/src/i18n/es.ts

@@ -40,6 +40,7 @@ export const dict = {
   "ui.messageNav.newMessage": "Nuevo mensaje",
 
   "ui.textField.copyToClipboard": "Copiar al portapapeles",
+  "ui.textField.copyLink": "Copiar enlace",
   "ui.textField.copied": "Copiado",
 
   "ui.imagePreview.alt": "Vista previa de imagen",

+ 1 - 0
packages/ui/src/i18n/fr.ts

@@ -40,6 +40,7 @@ export const dict = {
   "ui.messageNav.newMessage": "Nouveau message",
 
   "ui.textField.copyToClipboard": "Copier dans le presse-papiers",
+  "ui.textField.copyLink": "Copier le lien",
   "ui.textField.copied": "Copié",
 
   "ui.imagePreview.alt": "Aperçu de l'image",

+ 1 - 0
packages/ui/src/i18n/ja.ts

@@ -40,6 +40,7 @@ export const dict = {
   "ui.messageNav.newMessage": "新しいメッセージ",
 
   "ui.textField.copyToClipboard": "クリップボードにコピー",
+  "ui.textField.copyLink": "リンクをコピー",
   "ui.textField.copied": "コピーしました",
 
   "ui.imagePreview.alt": "画像プレビュー",

+ 1 - 0
packages/ui/src/i18n/ko.ts

@@ -40,6 +40,7 @@ export const dict = {
   "ui.messageNav.newMessage": "새 메시지",
 
   "ui.textField.copyToClipboard": "클립보드에 복사",
+  "ui.textField.copyLink": "링크 복사",
   "ui.textField.copied": "복사됨",
 
   "ui.imagePreview.alt": "이미지 미리보기",

+ 1 - 0
packages/ui/src/i18n/no.ts

@@ -43,6 +43,7 @@ export const dict: Record<Keys, string> = {
   "ui.messageNav.newMessage": "Ny melding",
 
   "ui.textField.copyToClipboard": "Kopier til utklippstavlen",
+  "ui.textField.copyLink": "Kopier lenke",
   "ui.textField.copied": "Kopiert",
 
   "ui.imagePreview.alt": "Bildeforhåndsvisning",

+ 1 - 0
packages/ui/src/i18n/pl.ts

@@ -40,6 +40,7 @@ export const dict = {
   "ui.messageNav.newMessage": "Nowa wiadomość",
 
   "ui.textField.copyToClipboard": "Skopiuj do schowka",
+  "ui.textField.copyLink": "Skopiuj link",
   "ui.textField.copied": "Skopiowano",
 
   "ui.imagePreview.alt": "Podgląd obrazu",

+ 1 - 0
packages/ui/src/i18n/ru.ts

@@ -40,6 +40,7 @@ export const dict = {
   "ui.messageNav.newMessage": "Новое сообщение",
 
   "ui.textField.copyToClipboard": "Копировать в буфер обмена",
+  "ui.textField.copyLink": "Копировать ссылку",
   "ui.textField.copied": "Скопировано",
 
   "ui.imagePreview.alt": "Предпросмотр изображения",

+ 1 - 0
packages/ui/src/i18n/zh.ts

@@ -44,6 +44,7 @@ export const dict = {
   "ui.messageNav.newMessage": "新消息",
 
   "ui.textField.copyToClipboard": "复制到剪贴板",
+  "ui.textField.copyLink": "复制链接",
   "ui.textField.copied": "已复制",
 
   "ui.imagePreview.alt": "图片预览",

+ 1 - 0
packages/ui/src/i18n/zht.ts

@@ -44,6 +44,7 @@ export const dict = {
   "ui.messageNav.newMessage": "新訊息",
 
   "ui.textField.copyToClipboard": "複製到剪貼簿",
+  "ui.textField.copyLink": "複製連結",
   "ui.textField.copied": "已複製",
 
   "ui.imagePreview.alt": "圖片預覽",

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

@@ -120,7 +120,7 @@
   --surface-brand-base: var(--yuzu-light-9);
   --surface-brand-hover: var(--yuzu-light-10);
   --surface-interactive-base: var(--cobalt-light-3);
-  --surface-interactive-hover: var(--cobalt-light-4);
+  --surface-interactive-hover: #e5f0ff;
   --surface-interactive-weak: var(--cobalt-light-2);
   --surface-interactive-weak-hover: var(--cobalt-light-3);
   --surface-success-base: var(--apple-light-3);
@@ -376,9 +376,9 @@
     --surface-raised-stronger-non-alpha: var(--smoke-dark-3);
     --surface-brand-base: var(--yuzu-light-9);
     --surface-brand-hover: var(--yuzu-light-10);
-    --surface-interactive-base: var(--cobalt-light-3);
-    --surface-interactive-hover: var(--cobalt-light-4);
-    --surface-interactive-weak: var(--cobalt-light-2);
+    --surface-interactive-base: var(--cobalt-dark-3);
+    --surface-interactive-hover: #0a1d4d;
+    --surface-interactive-weak: var(--cobalt-dark-2);
     --surface-interactive-weak-hover: var(--cobalt-light-3);
     --surface-success-base: var(--apple-light-3);
     --surface-success-weak: var(--apple-light-2);

+ 4 - 4
packages/ui/src/theme/themes/oc-1.json

@@ -46,7 +46,7 @@
       "surface-brand-base": "var(--yuzu-light-9)",
       "surface-brand-hover": "var(--yuzu-light-10)",
       "surface-interactive-base": "var(--cobalt-light-3)",
-      "surface-interactive-hover": "var(--cobalt-light-4)",
+      "surface-interactive-hover": "#E5F0FF",
       "surface-interactive-weak": "var(--cobalt-light-2)",
       "surface-interactive-weak-hover": "var(--cobalt-light-3)",
       "surface-success-base": "var(--apple-light-3)",
@@ -311,9 +311,9 @@
       "surface-raised-stronger-non-alpha": "var(--smoke-dark-3)",
       "surface-brand-base": "var(--yuzu-light-9)",
       "surface-brand-hover": "var(--yuzu-light-10)",
-      "surface-interactive-base": "var(--cobalt-light-3)",
-      "surface-interactive-hover": "var(--cobalt-light-4)",
-      "surface-interactive-weak": "var(--cobalt-light-2)",
+      "surface-interactive-base": "var(--cobalt-dark-3)",
+      "surface-interactive-hover": "#0A1D4D",
+      "surface-interactive-weak": "var(--cobalt-dark-2)",
       "surface-interactive-weak-hover": "var(--cobalt-light-3)",
       "surface-success-base": "var(--apple-dark-3)",
       "surface-success-weak": "var(--apple-dark-2)",

+ 1 - 1
packages/util/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/util",
-  "version": "1.1.34",
+  "version": "1.1.35",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/web/package.json

@@ -2,7 +2,7 @@
   "name": "@opencode-ai/web",
   "type": "module",
   "license": "MIT",
-  "version": "1.1.34",
+  "version": "1.1.35",
   "scripts": {
     "dev": "astro dev",
     "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",

+ 1 - 1
sdks/vscode/package.json

@@ -2,7 +2,7 @@
   "name": "opencode",
   "displayName": "opencode",
   "description": "opencode for VS Code",
-  "version": "1.1.34",
+  "version": "1.1.35",
   "publisher": "sst-dev",
   "repository": {
     "type": "git",