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

feat(app): ui improvements (#32438)

Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
Aarav Sareen пре 1 месец
родитељ
комит
1bcb9d7cb9
29 измењених фајлова са 415 додато и 131 уклоњено
  1. 22 0
      packages/app/src/components/command-tooltip-keybind.test.ts
  2. 11 0
      packages/app/src/components/command-tooltip-keybind.ts
  3. 6 2
      packages/app/src/components/prompt-input.tsx
  4. 1 1
      packages/app/src/components/server/server-row-menu.tsx
  5. 1 1
      packages/app/src/components/server/server-row.tsx
  6. 19 4
      packages/app/src/components/session/session-header.tsx
  7. 5 1
      packages/app/src/components/titlebar-session-events.test.ts
  8. 6 0
      packages/app/src/components/titlebar-session-events.ts
  9. 4 2
      packages/app/src/components/titlebar-tab-nav.tsx
  10. 26 0
      packages/app/src/components/titlebar.css
  11. 24 12
      packages/app/src/components/titlebar.tsx
  12. 5 0
      packages/app/src/context/command.tsx
  13. 7 6
      packages/app/src/context/tabs.tsx
  14. 1 0
      packages/app/src/i18n/en.ts
  15. 1 0
      packages/app/src/i18n/zh.ts
  16. 51 0
      packages/app/src/pages/home-session-archive.test.ts
  17. 37 0
      packages/app/src/pages/home-session-archive.ts
  18. 164 84
      packages/app/src/pages/home.tsx
  19. 2 2
      packages/ui/src/components/dialog.css
  20. 5 1
      packages/ui/src/components/scroll-view.css
  21. 1 1
      packages/ui/src/theme/themes/oc-2.json
  22. 1 1
      packages/ui/src/theme/v2/mapping.ts
  23. 2 2
      packages/ui/src/v2/components/dialog-v2.css
  24. 2 6
      packages/ui/src/v2/components/icon-button-v2.css
  25. 4 0
      packages/ui/src/v2/components/icon.tsx
  26. 2 2
      packages/ui/src/v2/components/project-avatar-v2.css
  27. 1 1
      packages/ui/src/v2/components/select-v2.css
  28. 3 1
      packages/ui/src/v2/components/tabs-v2.css
  29. 1 1
      packages/ui/src/v2/styles/theme.css

+ 22 - 0
packages/app/src/components/command-tooltip-keybind.test.ts

@@ -0,0 +1,22 @@
+import { describe, expect, test } from "bun:test"
+import { newTabTooltipKeybind, reviewTooltipKeybind } from "./command-tooltip-keybind"
+
+describe("command tooltip keybinds", () => {
+  test("keeps localized review shortcut modifiers", () => {
+    const command = {
+      keybind: () => "Ctrl+Maj+R",
+      keybindParts: () => ["Ctrl", "Maj", "R"],
+    }
+
+    expect(reviewTooltipKeybind(command, (key) => key)).toEqual(["Ctrl", "Maj", "R"])
+  })
+
+  test("uses the configured new-tab shortcut", () => {
+    const command = {
+      keybind: () => "Alt+N",
+      keybindParts: () => ["Alt", "N"],
+    }
+
+    expect(newTabTooltipKeybind(command, (key) => key)).toEqual(["Alt", "N"])
+  })
+})

+ 11 - 0
packages/app/src/components/command-tooltip-keybind.ts

@@ -0,0 +1,11 @@
+type CommandKeybind = {
+  keybindParts: (id: string) => string[]
+}
+
+export function reviewTooltipKeybind(command: CommandKeybind, _translate?: (key: string) => string) {
+  return command.keybindParts("review.toggle")
+}
+
+export function newTabTooltipKeybind(command: CommandKeybind, _translate?: (key: string) => string) {
+  return command.keybindParts("tab.new")
+}

+ 6 - 2
packages/app/src/components/prompt-input.tsx

@@ -2196,7 +2196,9 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) {
                 )}
               </Show>
               <span class="truncate">{props.state.modelName}</span>
-              <Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
+              <span class="-ml-1 shrink-0 flex size-fit">
+                <Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" />
+              </span>
             </Button>
           </TooltipKeybind>
         }
@@ -2225,7 +2227,9 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) {
               )}
             </Show>
             <span class="truncate">{props.state.modelName}</span>
-            <Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
+            <span class="-ml-1 shrink-0 flex size-fit">
+              <Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" />
+            </span>
           </ModelSelectorPopover>
         </TooltipKeybind>
       </Show>

+ 1 - 1
packages/app/src/components/server/server-row-menu.tsx

@@ -19,7 +19,7 @@ export const ServerRowMenu: Component<{
   const isDefault = () => props.controller.defaultKey() === key
 
   return (
-    <MenuV2 gutter={4} modal={false} placement="bottom-end" open={props.open} onOpenChange={props.onOpenChange}>
+    <MenuV2 gutter={6} modal={false} placement="bottom-end" open={props.open} onOpenChange={props.onOpenChange}>
       <MenuV2.Trigger
         as={IconButtonV2}
         variant="ghost-muted"

+ 1 - 1
packages/app/src/components/server/server-row.tsx

@@ -117,7 +117,7 @@ export function ServerHealthIndicator(props: { health?: ServerHealth }) {
   return (
     <div
       classList={{
-        "size-1.5 rounded-full shrink-0": true,
+        "size-1.5 rounded-full shrink-0 my-[3.5px]": true,
         "bg-icon-success-base": props.health?.healthy === true,
         "bg-icon-critical-base": props.health?.healthy === false,
         "bg-border-weak-base": props.health === undefined,

+ 19 - 4
packages/app/src/components/session/session-header.tsx

@@ -28,6 +28,9 @@ import { Persist, persisted } from "@/utils/persist"
 import { StatusPopover, StatusPopoverV2 } from "../status-popover"
 import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
 import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
+import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
+import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
+import { reviewTooltipKeybind } from "../command-tooltip-keybind"
 
 const OPEN_APPS = [
   "vscode",
@@ -237,7 +240,7 @@ export function SessionHeader() {
     statusVisible: status(),
     statusLabel: language.t("status.popover.trigger"),
     reviewLabel: language.t("command.review.toggle"),
-    reviewKeybind: command.keybind("review.toggle"),
+    reviewKeybind: reviewTooltipKeybind(command),
     reviewVisible: isDesktop(),
     reviewOpened: view().reviewPanel.opened(),
     onReviewToggle: () => view().reviewPanel.toggle(),
@@ -520,13 +523,15 @@ type SessionHeaderV2ActionsState = {
   statusVisible: boolean
   statusLabel: string
   reviewLabel: string
-  reviewKeybind: string
+  reviewKeybind: string[]
   reviewVisible: boolean
   reviewOpened: boolean
   onReviewToggle: () => void
 }
 
 function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
+  const language = useLanguage()
+
   return (
     <div class="flex items-center gap-2">
       <Show when={props.state.statusVisible}>
@@ -535,7 +540,17 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
         </Tooltip>
       </Show>
       <Show when={props.state.reviewVisible}>
-        <TooltipKeybind title={props.state.reviewLabel} keybind={props.state.reviewKeybind}>
+        <TooltipV2
+          placement="bottom"
+          value={
+            <>
+              {props.state.reviewLabel}
+              <Show when={props.state.reviewKeybind.length > 0}>
+                <KeybindV2 keys={props.state.reviewKeybind} variant="neutral" />
+              </Show>
+            </>
+          }
+        >
           <IconButtonV2
             type="button"
             variant="ghost-muted"
@@ -548,7 +563,7 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
             aria-controls="review-panel"
             icon={<IconV2 name="sidebar-right" />}
           />
-        </TooltipKeybind>
+        </TooltipV2>
       </Show>
     </div>
   )

+ 5 - 1
packages/app/src/components/titlebar-session-events.test.ts

@@ -1,15 +1,19 @@
 import { describe, expect, test } from "bun:test"
+import type { ServerConnection } from "@/context/server"
 import { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "./titlebar-session-events"
 
+const remote = "remote" as ServerConnection.Key
+
 describe("titlebar session events", () => {
   test("reads valid removed session tab details", () => {
     expect(
       readSessionTabsRemovedDetail(
         new CustomEvent(SESSION_TABS_REMOVED_EVENT, {
-          detail: { directory: "/tmp/project", sessionIDs: ["ses_1", "ses_2", 1] },
+          detail: { server: "remote", directory: "/tmp/project", sessionIDs: ["ses_1", "ses_2", 1] },
         }),
       ),
     ).toEqual({
+      server: remote,
       directory: "/tmp/project",
       sessionIDs: ["ses_1", "ses_2"],
     })

+ 6 - 0
packages/app/src/components/titlebar-session-events.ts

@@ -1,6 +1,9 @@
+import type { ServerConnection } from "@/context/server"
+
 export const SESSION_TABS_REMOVED_EVENT = "opencode:session-tabs-removed"
 
 export type SessionTabsRemovedDetail = {
+  server?: ServerConnection.Key
   directory: string
   sessionIDs: string[]
 }
@@ -18,11 +21,14 @@ export function readSessionTabsRemovedDetail(event: Event): SessionTabsRemovedDe
   if (!("sessionIDs" in detail)) return undefined
   if (typeof detail.directory !== "string") return undefined
   if (!Array.isArray(detail.sessionIDs)) return undefined
+  if ("server" in detail && detail.server !== undefined && typeof detail.server !== "string") return undefined
 
   const sessionIDs = detail.sessionIDs.filter((id): id is string => typeof id === "string")
   if (sessionIDs.length === 0) return undefined
 
   return {
+    server:
+      "server" in detail && typeof detail.server === "string" ? (detail.server as ServerConnection.Key) : undefined,
     directory: detail.directory,
     sessionIDs,
   }

+ 4 - 2
packages/app/src/components/titlebar-tab-nav.tsx

@@ -175,9 +175,10 @@ export function TabNavItem(props: {
         forwardTabRef(props.ref, el)
       }}
       data-titlebar-tab
+      data-slot="titlebar-tab-item"
       data-title-overflow={titleOverflowing()}
       data-editing={editing()}
-      class="group relative flex h-7 min-w-24 max-w-60 select-none flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[editing='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
+      class="group relative flex h-7 min-w-24 max-w-60 select-none flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[editing='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
       classList={{ invisible: props.hidden }}
       data-active={props.active}
       data-dragging={props.dragging}
@@ -292,10 +293,11 @@ export function DraftTabItem(props: {
     <div
       ref={(el) => forwardTabRef(props.ref, el)}
       data-titlebar-tab
+      data-slot="titlebar-tab-item"
       data-active={props.active}
       data-dragging={props.dragging}
       data-pressed={props.pressed}
-      class="group relative shrink-0 flex h-7 max-w-60 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--tab-bg)] pl-1.5 pr-8 whitespace-nowrap [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-overlay-simple-overlay-pressed)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-[var(--v2-border-border-focus)]"
+      class="group relative shrink-0 flex h-7 max-w-60 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--tab-bg)] pl-1.5 pr-8 whitespace-nowrap [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-overlay-simple-overlay-pressed)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
       classList={{ invisible: props.hidden }}
       onMouseDown={(event) => {
         if (event.button !== 1) return

+ 26 - 0
packages/app/src/components/titlebar.css

@@ -1,3 +1,29 @@
+[data-slot="titlebar-tab-item"] {
+  user-select: none;
+}
+
+[data-slot="titlebar-tab-item"] a {
+  outline: none;
+}
+
+[data-slot="titlebar-v2"] [data-component="icon-button-v2"][data-variant="ghost-muted"]:is(:focus-visible, [data-state="focus"]):not(:disabled) {
+  outline: none;
+  background-color: var(--v2-overlay-simple-overlay-hover);
+}
+
+[data-slot="titlebar-tab-item"] [data-component="icon-button-v2"]:is(:focus-visible, [data-state="focus"]):not(:disabled) {
+  opacity: 1;
+}
+
+[data-slot="titlebar-tab-item"]:has([data-component="icon-button-v2"]:focus-visible) [data-slot="titlebar-tab-close"] {
+  right: 0;
+  left: auto;
+}
+
+[data-slot="titlebar-tab-item"]:has([data-component="icon-button-v2"]:focus-visible) [data-slot="titlebar-tab-close-fade"] {
+  background-image: var(--active-bg);
+}
+
 @keyframes titlebar-tab-fade-left {
   from {
     visibility: hidden;

+ 24 - 12
packages/app/src/components/titlebar.tsx

@@ -26,6 +26,7 @@ import { useGlobal } from "@/context/global"
 import { ServerConnection, useServer } from "@/context/server"
 import { tabKey, useTabs } from "@/context/tabs"
 import "./titlebar.css"
+import { newTabTooltipKeybind } from "./command-tooltip-keybind"
 
 type TauriDesktopWindow = {
   startDragging?: () => Promise<void>
@@ -217,6 +218,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
 
   return (
     <header
+      data-slot={useV2Titlebar() ? "titlebar-v2" : undefined}
       classList={{
         "shrink-0 relative flex flex-row": true,
         "h-9 bg-v2-background-bg-deep overflow-visible": useV2Titlebar(),
@@ -459,15 +461,25 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
                   onReorder={(keys) => tabsStoreActions.reorder(keys)}
                 />
                 <Show when={!(creating() && params.dir)}>
-                  <IconButtonV2
-                    type="button"
-                    variant="ghost-muted"
-                    size="large"
-                    class="shrink-0"
-                    icon={<IconV2 name="plus" />}
-                    onClick={openNewTab}
-                    aria-label={language.t("command.session.new")}
-                  />
+                  <TooltipV2
+                    placement="bottom"
+                    value={
+                      <>
+                        {language.t("command.session.new")}
+                        <KeybindV2 keys={newTabTooltipKeybind(command)} variant="neutral" />
+                      </>
+                    }
+                  >
+                    <IconButtonV2
+                      type="button"
+                      variant="ghost-muted"
+                      size="large"
+                      class="shrink-0"
+                      icon={<IconV2 name="plus" />}
+                      onClick={openNewTab}
+                      aria-label={language.t("command.session.new")}
+                    />
+                  </TooltipV2>
                 </Show>
                 <div class="flex-1" />
                 <TitlebarV2Right state={v2RightState()} />
@@ -663,16 +675,16 @@ function TitlebarV2Right(props: { state: TitlebarV2RightState }) {
 
 function TitlebarUpdateIconButton(props: { state: TitlebarUpdatePillState }) {
   return (
-    <div class="relative isolate mr-3 size-5 shrink-0">
+    <div class="group relative mr-3 h-5 w-5 shrink-0 rounded-full bg-v2-background-bg-deep transition-[width] duration-150 ease-out hover:z-30 hover:w-[68px] focus-within:z-30 focus-within:w-[68px] motion-reduce:transition-none">
       <button
         type="button"
-        class="group absolute right-0 top-0 z-10 flex h-5 w-5 items-center justify-end overflow-hidden rounded-full bg-v2-icon-icon-accent/20 text-v2-icon-icon-accent transition-[width,background-color] duration-150 ease-out hover:z-30 hover:w-[68px] hover:bg-[color-mix(in_srgb,var(--v2-icon-icon-accent)_20%,var(--v2-background-bg-deep))] focus-visible:z-30 focus-visible:w-[68px] focus-visible:bg-[color-mix(in_srgb,var(--v2-icon-icon-accent)_20%,var(--v2-background-bg-deep))] focus-visible:outline-none disabled:opacity-60 motion-reduce:transition-none"
+        class="absolute right-0 top-0 z-10 flex h-5 w-5 items-center justify-end overflow-hidden rounded-full bg-v2-icon-icon-accent/20 text-v2-icon-icon-accent transition-[width,background-color] duration-150 ease-out group-hover:w-[68px] group-hover:bg-[color-mix(in_srgb,var(--v2-icon-icon-accent)_20%,var(--v2-background-bg-deep))] group-focus-within:w-[68px] group-focus-within:bg-[color-mix(in_srgb,var(--v2-icon-icon-accent)_20%,var(--v2-background-bg-deep))] focus-visible:outline-none disabled:opacity-60 motion-reduce:transition-none"
         onClick={props.state.onInstall}
         disabled={props.state.installing}
         aria-busy={props.state.installing}
         aria-label={props.state.ariaLabel}
       >
-        <span class="shrink-0 ml-[8px] mr-px text-[11px] text-v2-text-text-accent [font-weight:530] opacity-0 translate-x-2 motion-safe:transition-all duration-150 ease-out group-hover:opacity-100 group-hover:translate-x-0 group-focus-visible:opacity-100 group-focus-visible:translate-x-0 motion-reduce:translate-x-0">
+        <span class="shrink-0 ml-[8px] mr-px text-[11px] text-v2-text-text-accent [font-weight:530] opacity-0 translate-x-2 motion-safe:transition-all duration-150 ease-out group-hover:opacity-100 group-hover:translate-x-0 group-focus-within:opacity-100 group-focus-within:translate-x-0 motion-reduce:translate-x-0">
           Update
         </span>
         <span class="flex size-5 shrink-0 items-center justify-center">

+ 5 - 0
packages/app/src/context/command.tsx

@@ -227,6 +227,11 @@ export function formatKeybind(config: string, t?: (key: KeyLabel) => string): st
   return IS_MAC ? parts.join("") : parts.join("+")
 }
 
+// KeybindV2 takes an array instead of a string
+export function formatKeybindKeys(config: string, t?: (key: KeyLabel) => string): string[] {
+  return formatKeybindParts(config, t)
+}
+
 function isEditableTarget(target: EventTarget | null) {
   if (!(target instanceof HTMLElement)) return false
   if (target.isContentEditable) return true

+ 7 - 6
packages/app/src/context/tabs.tsx

@@ -205,9 +205,10 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
         if (server.key === key) navigate("/")
       },
       removeSessions: (input: SessionTabsRemovedDetail) => {
+        const targetServer = input.server ?? server.key
         const removed = store
           .filter(
-            (tab) => tab.type === "session" && tab.server === server.key && input.sessionIDs.includes(tab.sessionId),
+            (tab) => tab.type === "session" && tab.server === targetServer && input.sessionIDs.includes(tab.sessionId),
           )
           .map(tabKey)
         void startTransition(() => {
@@ -215,28 +216,28 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
             produce((tabs) => {
               const sessionIDs = new Set(input.sessionIDs)
               const currentHref =
-                params.dir && params.id
+                targetServer === server.key && params.dir && params.id
                   ? tabHref({
                       type: "session",
-                      server: server.key,
+                      server: targetServer,
                       sessionId: params.id,
                     })
                   : undefined
               const currentIndex = currentHref
                 ? tabs.findIndex(
-                    (tab) => tab.type === "session" && tab.server === server.key && tabHref(tab) === currentHref,
+                    (tab) => tab.type === "session" && tab.server === targetServer && tabHref(tab) === currentHref,
                   )
                 : -1
               const currentTab = tabs[currentIndex]
               const removedCurrent =
                 currentTab?.type === "session" &&
-                currentTab.server === server.key &&
+                currentTab.server === targetServer &&
                 sessionIDs.has(currentTab.sessionId)
 
               for (let i = tabs.length - 1; i >= 0; i--) {
                 const tab = tabs[i]
                 if (!tab || tab.type !== "session") continue
-                if (tab.server !== server.key) continue
+                if (tab.server !== targetServer) continue
                 if (!sessionIDs.has(tab.sessionId)) continue
                 tabs.splice(i, 1)
               }

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

@@ -592,6 +592,7 @@ export const dict = {
   "home.server.collapse": "Collapse server projects",
   "home.server.expand": "Expand server projects",
   "home.sessions.search.placeholder": "Search sessions",
+  "home.sessions.search.placeholder.scoped": "Search sessions in {{scope}}",
   "home.sessions.search.sessions": "Sessions",
   "home.sessions.search.noResults": "No sessions found for {{query}}",
   "home.sessions.empty": "No sessions found",

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

@@ -507,6 +507,7 @@ export const dict = {
   "home.projects": "项目",
   "home.project.add": "添加项目",
   "home.sessions.search.placeholder": "搜索会话",
+  "home.sessions.search.placeholder.scoped": "在 {{scope}} 中搜索会话",
   "home.sessions.search.sessions": "会话",
   "home.sessions.search.noResults": "未找到与 {{query}} 相关的会话",
   "home.sessions.empty": "未找到会话",

+ 51 - 0
packages/app/src/pages/home-session-archive.test.ts

@@ -0,0 +1,51 @@
+import { expect, test } from "bun:test"
+import { SESSION_TABS_REMOVED_EVENT, readSessionTabsRemovedDetail } from "@/components/titlebar-session-events"
+import { archiveHomeSession } from "./home-session-archive"
+import type { ServerConnection } from "@/context/server"
+
+const remote = "remote" as ServerConnection.Key
+
+test("archiving a Home session removes its open titlebar tab", async () => {
+  let detail: ReturnType<typeof readSessionTabsRemovedDetail>
+  let removed = false
+  window.addEventListener(
+    SESSION_TABS_REMOVED_EVENT,
+    (event) => {
+      detail = readSessionTabsRemovedDetail(event)
+    },
+    { once: true },
+  )
+
+  await archiveHomeSession({
+    server: remote,
+    session: { id: "ses_1", directory: "/workspace" },
+    update: async () => undefined,
+    remove: () => {
+      removed = true
+    },
+  })
+
+  expect(removed).toBe(true)
+  expect(detail).toEqual({ server: remote, directory: "/workspace", sessionIDs: ["ses_1"] })
+})
+
+test("reports archive failures without removing the session", async () => {
+  const failure = new Error("offline")
+  let error: unknown
+  let removed = false
+
+  await archiveHomeSession({
+    server: remote,
+    session: { id: "ses_1", directory: "/workspace" },
+    update: async () => Promise.reject(failure),
+    remove: () => {
+      removed = true
+    },
+    onError: (value) => {
+      error = value
+    },
+  })
+
+  expect(error).toBe(failure)
+  expect(removed).toBe(false)
+})

+ 37 - 0
packages/app/src/pages/home-session-archive.ts

@@ -0,0 +1,37 @@
+import { notifySessionTabsRemoved } from "@/components/titlebar-session-events"
+import type { ServerConnection } from "@/context/server"
+
+type HomeSession = {
+  id: string
+  directory: string
+}
+
+type SessionUpdate = {
+  directory: string
+  sessionID: string
+  time: { archived: number }
+}
+
+export async function archiveHomeSession(input: {
+  server: ServerConnection.Key
+  session: HomeSession
+  update: (value: SessionUpdate) => Promise<unknown>
+  remove: () => void
+  onError?: (error: unknown) => void
+}) {
+  await input
+    .update({
+      directory: input.session.directory,
+      sessionID: input.session.id,
+      time: { archived: Date.now() },
+    })
+    .then(() => {
+      input.remove()
+      notifySessionTabsRemoved({
+        server: input.server,
+        directory: input.session.directory,
+        sessionIDs: [input.session.id],
+      })
+    })
+    .catch((error) => input.onError?.(error))
+}

+ 164 - 84
packages/app/src/pages/home.tsx

@@ -14,7 +14,7 @@ import {
   Switch,
 } from "solid-js"
 import { makeEventListener } from "@solid-primitives/event-listener"
-import { createStore } from "solid-js/store"
+import { createStore, produce } from "solid-js/store"
 import { useQuery } from "@tanstack/solid-query"
 import { Button } from "@opencode-ai/ui/button"
 import { Logo } from "@opencode-ai/ui/logo"
@@ -25,6 +25,7 @@ import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
 import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
 import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
 import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
+import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
 import { getProjectAvatarVariant, useLayout, type LocalProject } from "@/context/layout"
 import { useNavigate } from "@solidjs/router"
 import { base64Encode } from "@opencode-ai/core/util/encode"
@@ -35,7 +36,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { useDirectoryPicker } from "@/components/directory-picker"
 import { DialogSelectServer, useServerManagementController } from "@/components/dialog-select-server"
 import { DialogServerV2 } from "@/components/settings-v2/dialog-server-v2"
-import { ServerConnection, useServer } from "@/context/server"
+import { ServerConnection, serverName, useServer } from "@/context/server"
 import { sessionHasOpenTab, useTabs } from "@/context/tabs"
 import { useServerSync, type ServerSync } from "@/context/server-sync"
 import { useLanguage } from "@/context/language"
@@ -43,6 +44,7 @@ import { useNotification } from "@/context/notification"
 import {
   closeHomeProject,
   displayName,
+  errorMessage,
   getProjectAvatarSource,
   homeProjectDirectories,
   type HomeProjectSelection,
@@ -55,12 +57,15 @@ import { sessionTitle } from "@/utils/session-title"
 import { pathKey } from "@/utils/path-key"
 import { useGlobal } from "@/context/global"
 import { useCommand } from "@/context/command"
+import { Binary } from "@opencode-ai/core/util/binary"
 import { ServerRowMenu } from "@/components/server/server-row-menu"
 import { ServerHealthIndicator } from "@/components/server/server-row"
 import { type ServerHealth } from "@/utils/server-health"
 import { Persist, persisted } from "@/utils/persist"
 import { useMarked } from "@opencode-ai/ui/context/marked"
 import { preloadMarkdown } from "@opencode-ai/session-ui/markdown-cache"
+import { archiveHomeSession } from "./home-session-archive"
+import { showToast } from "@/utils/toast"
 
 const HOME_SESSION_LIMIT = 64
 const HOME_ROW_LAYOUT =
@@ -85,7 +90,7 @@ type HomeSessionGroup = {
 
 const HOME_SESSION_SEARCH_RESULTS_ID = "home-session-search-results"
 const HOME_SEARCH_RESULT_ROW =
-  "flex h-10 w-full shrink-0 cursor-default items-center gap-2 border-0 py-3 pl-4 pr-6 text-left transition-[background-color] duration-[120ms] ease-in-out hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
+  "flex h-10 w-full shrink-0 cursor-default items-center gap-2 border-0 py-3 pl-[18px] pr-6 text-left transition-[background-color] duration-[120ms] ease-in-out hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
 const HOME_SEARCH_RESULT_TITLE =
   "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-base [font-weight:530]"
 const HOME_SEARCH_RESULT_META =
@@ -172,6 +177,19 @@ export function NewHome() {
     return directories(project)
   })
   const search = createMemo(() => state.search.trim())
+  const searchPlaceholder = createMemo(() => {
+    const project = selectedProject()
+    if (project) {
+      return language.t("home.sessions.search.placeholder.scoped", { scope: displayName(project) })
+    }
+    if (global.servers.list().length > 1) {
+      const conn = focusedServer()
+      if (conn) {
+        return language.t("home.sessions.search.placeholder.scoped", { scope: serverName(conn) })
+      }
+    }
+    return language.t("home.sessions.search.placeholder")
+  })
   const sessionLoad = useQuery(() => ({
     queryKey: ["home", "sessions", state.selection.server, ...projectDirectories()] as const,
     queryFn: async () => {
@@ -258,7 +276,7 @@ export function NewHome() {
   command.register("home", () => [
     {
       id: "home.sessions.search.focus",
-      title: language.t("home.sessions.search.placeholder"),
+      title: searchPlaceholder(),
       keybind: "mod+f",
       hidden: true,
       onSelect: () => focusSessionSearch?.(),
@@ -350,6 +368,30 @@ export function NewHome() {
     })
   }
 
+  async function archiveSession(session: Session) {
+    const conn = focusedServer()
+    const ctx = focusedServerCtx()
+    if (!conn || !ctx) return
+    const [, setStore] = ctx.sync.child(session.directory)
+    await archiveHomeSession({
+      server: ServerConnection.key(conn),
+      session,
+      update: (value) => ctx.sdk.client.session.update(value),
+      remove: () =>
+        setStore(
+          produce((draft) => {
+            const match = Binary.search(draft.session, session.id, (s) => s.id)
+            if (match.found) draft.session.splice(match.index, 1)
+          }),
+        ),
+      onError: (error) =>
+        showToast({
+          title: language.t("common.requestFailed"),
+          description: errorMessage(error, language.t("common.requestFailed")),
+        }),
+    })
+  }
+
   function chooseProject(conn: ServerConnection.Any) {
     function resolve(result: string | string[] | null) {
       addProjects(conn, homeProjectDirectories(result))
@@ -402,10 +444,11 @@ export function NewHome() {
         >
           <HomeSessionSearch
             value={state.search}
-            placeholder={language.t("home.sessions.search.placeholder")}
+            placeholder={searchPlaceholder()}
             open={searchOpen()}
             loading={sessionLoad.isLoading}
             results={searchResults()}
+            showProjectName={!selectedProject()}
             server={state.selection.server}
             activeServer={state.selection.server === server.key}
             noResultsLabel={language.t("home.sessions.search.noResults", { query: search() })}
@@ -446,9 +489,11 @@ export function NewHome() {
                             {(record) => (
                               <HomeSessionRow
                                 record={record}
+                                showProjectName={!selectedProject()}
                                 server={state.selection.server}
                                 activeServer={state.selection.server === server.key}
-                                onClick={() => openSession(record.session)}
+                                openSession={openSession}
+                                archiveSession={archiveSession}
                               />
                             )}
                           </For>
@@ -502,15 +547,17 @@ function HomeProjectColumn(props: {
       <div class="flex h-7 min-w-0 items-center justify-between pl-1.5">
         <div class={HOME_SECTION_LABEL}>{props.language.t("home.projects")}</div>
         <Show when={global.servers.list().length === 1}>
-          <IconButtonV2
-            data-action="home-add-project"
-            variant="ghost-muted"
-            size="large"
-            class="titlebar-icon [&_[data-slot=icon-svg]]:text-v2-icon-icon-muted"
-            icon={<IconV2 name="folder-add-left" />}
-            onClick={() => props.chooseProject(global.servers.list()[0]!)}
-            aria-label={props.language.t("home.project.add")}
-          />
+          <TooltipV2 placement="bottom" value={props.language.t("home.project.add")}>
+            <IconButtonV2
+              data-action="home-add-project"
+              variant="ghost-muted"
+              size="large"
+              class="titlebar-icon [&_[data-slot=icon-svg]]:text-v2-icon-icon-muted"
+              icon={<IconV2 name="folder-add-left" />}
+              onClick={() => props.chooseProject(global.servers.list()[0]!)}
+              aria-label={props.language.t("home.project.add")}
+            />
+          </TooltipV2>
         </Show>
       </div>
       <Show
@@ -646,7 +693,7 @@ function HomeServerRow(props: {
         </span>
       </button>
       <div
-        class="absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5 opacity-0 transition-opacity group-hover/server:opacity-100 focus-within:opacity-100 data-[menu=true]:opacity-100"
+        class="absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-1 opacity-0 transition-opacity group-hover/server:opacity-100 focus-within:opacity-100 data-[menu=true]:opacity-100"
         data-menu={state.menuOpen}
       >
         <ServerRowMenu
@@ -656,14 +703,20 @@ function HomeServerRow(props: {
           open={state.menuOpen}
           onOpenChange={(open) => setState("menuOpen", open)}
         />
-        <IconButtonV2
-          data-action="home-add-project"
-          variant="ghost-muted"
-          size="small"
-          icon={<IconV2 name="folder-add-left" />}
-          aria-label={props.language.t("home.project.add")}
-          onClick={() => props.chooseProject(props.server)}
-        />
+        <TooltipV2
+          class="flex shrink-0 items-center"
+          placement="bottom"
+          value={props.language.t("home.project.add")}
+        >
+          <IconButtonV2
+            data-action="home-add-project"
+            variant="ghost-muted"
+            size="small"
+            icon={<IconV2 name="folder-add-left" />}
+            aria-label={props.language.t("home.project.add")}
+            onClick={() => props.chooseProject(props.server)}
+          />
+        </TooltipV2>
       </div>
     </div>
   )
@@ -733,19 +786,11 @@ function HomeProjectRow(props: {
         <span class={HOME_PROJECT_NAV_LABEL}>{displayName(props.project)}</span>
       </button>
       <div
-        class="absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5 opacity-0 transition-opacity group-hover/project:opacity-100 focus-within:opacity-100 data-[menu=true]:opacity-100"
+        class="absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-1 opacity-0 transition-opacity group-hover/project:opacity-100 focus-within:opacity-100 data-[menu=true]:opacity-100"
         data-menu={state.menuOpen}
       >
-        <IconButtonV2
-          data-action="home-project-new-session"
-          variant="ghost-muted"
-          size="small"
-          icon={<IconV2 name="edit" />}
-          aria-label={props.language.t("command.session.new")}
-          onClick={() => props.openNewSession(props.server, props.project.worktree)}
-        />
         <MenuV2
-          gutter={4}
+          gutter={6}
           modal={false}
           placement="bottom-end"
           open={state.menuOpen}
@@ -765,7 +810,7 @@ function HomeProjectRow(props: {
                 {props.language.t("command.session.new")}
               </MenuV2.Item>
               <MenuV2.Item onSelect={() => props.editProject(props.server, props.project)}>
-                {props.language.t("common.edit")}
+                {props.language.t("dialog.project.edit.title")}
               </MenuV2.Item>
               <MenuV2.Item
                 disabled={props.unseenCount === 0}
@@ -780,6 +825,14 @@ function HomeProjectRow(props: {
             </MenuV2.Content>
           </MenuV2.Portal>
         </MenuV2>
+        <IconButtonV2
+          data-action="home-project-new-session"
+          variant="ghost-muted"
+          size="small"
+          icon={<IconV2 name="edit" />}
+          aria-label={props.language.t("command.session.new")}
+          onClick={() => props.openNewSession(props.server, props.project.worktree)}
+        />
       </div>
     </div>
   )
@@ -810,7 +863,7 @@ function HomeSessionLeading(props: {
         <span
           aria-hidden="true"
           class="pointer-events-none absolute top-1/2 h-[7px] w-[3px] -translate-y-1/2 rounded-[2px] bg-v2-background-bg-layer-04"
-          style={{ right: "calc(100% + 12px)" }}
+          style={{ right: "calc(100% + 5px)" }}
         />
       </Show>
       <SessionTabAvatar
@@ -829,6 +882,7 @@ function HomeSessionSearch(props: {
   open: boolean
   loading: boolean
   results: HomeSessionRecord[]
+  showProjectName: boolean
   server: ServerConnection.Key
   activeServer: boolean
   noResultsLabel: string
@@ -906,12 +960,12 @@ function HomeSessionSearch(props: {
   )
 
   return (
-    <div class="ml-4 mr-2 w-[calc(100%_-_24px)]">
+    <div class="mr-2 w-[calc(100%_-_8px)]">
       <div ref={root} data-component="home-session-search" class="relative z-10 w-full">
         <Show when={props.open}>
           <div
             data-component="home-session-search-panel"
-            class="absolute flex flex-col rounded-[12px] bg-v2-background-bg-base shadow-[var(--v2-elevation-floating)]"
+            class="absolute flex flex-col overflow-hidden rounded-[12px] bg-v2-background-bg-base shadow-[var(--v2-elevation-floating)]"
             style={{
               top: "-6px",
               left: "-6px",
@@ -919,7 +973,7 @@ function HomeSessionSearch(props: {
             }}
           >
             <div class="flex flex-col pt-9">
-              <div id={HOME_SESSION_SEARCH_RESULTS_ID} role="listbox" class="flex flex-col gap-4 pt-4 pb-2">
+              <div id={HOME_SESSION_SEARCH_RESULTS_ID} role="listbox" class="flex flex-col gap-4 pt-4">
                 <Show
                   when={!props.loading}
                   fallback={
@@ -931,29 +985,32 @@ function HomeSessionSearch(props: {
                   <Show
                     when={props.results.length > 0}
                     fallback={
-                      <p class="my-1.5 px-4 text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-muted [font-weight:440]">
+                      <p class="my-1.5 px-4 pb-2 text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-muted [font-weight:440]">
                         {props.noResultsLabel}
                       </p>
                     }
                   >
                     <div class="flex flex-col">
-                      <p class="my-1.5 px-4 text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-muted [font-weight:440]">
+                      <p class="my-1.5 pl-[18px] pr-6 text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-muted [font-weight:440]">
                         {language.t("home.sessions.search.sessions")}
                       </p>
-                      <div ref={listRef} class="flex max-h-80 flex-col gap-px overflow-y-auto">
-                        <For each={props.results}>
-                          {(record) => (
-                            <HomeSessionSearchResultRow
-                              record={record}
-                              server={props.server}
-                              activeServer={props.activeServer}
-                              selected={store.active === homeSessionSearchKey(record)}
-                              onHighlight={() => setStore("active", homeSessionSearchKey(record))}
-                              onSelect={(session) => props.onSelect(session)}
-                            />
-                          )}
-                        </For>
-                      </div>
+                      <ScrollView class="max-h-80" viewportRef={(el) => (listRef = el)}>
+                        <div class="flex flex-col gap-px pb-2">
+                          <For each={props.results}>
+                            {(record) => (
+                              <HomeSessionSearchResultRow
+                                record={record}
+                                showProjectName={props.showProjectName}
+                                server={props.server}
+                                activeServer={props.activeServer}
+                                selected={store.active === homeSessionSearchKey(record)}
+                                onHighlight={() => setStore("active", homeSessionSearchKey(record))}
+                                onSelect={(session) => props.onSelect(session)}
+                              />
+                            )}
+                          </For>
+                        </div>
+                      </ScrollView>
                     </div>
                   </Show>
                 </Show>
@@ -962,11 +1019,10 @@ function HomeSessionSearch(props: {
           </div>
         </Show>
         <label
-          class="relative z-20 flex h-9 w-full items-center gap-2 rounded-[6px] py-1 pl-3 pr-2 text-v2-icon-icon-muted transition-[background-color,box-shadow] duration-[120ms] ease-in-out"
+          class="relative z-20 flex h-9 w-full items-center gap-2 rounded-[6px] bg-v2-background-bg-layer-02 py-1 pl-3 pr-2 text-v2-icon-icon-muted transition-[background-color,box-shadow] duration-[120ms] ease-in-out"
           classList={{
-            "bg-v2-background-bg-layer-03 focus-within:bg-v2-background-bg-layer-03 focus-within:shadow-[0_0_0_0.5px_var(--v2-border-border-focus),var(--v2-elevation-raised)]":
-              !props.open,
-            "bg-transparent shadow-[0_0_0_0.5px_var(--v2-border-border-focus)]": props.open,
+            "focus-within:shadow-[0_0_0_0.5px_var(--v2-border-border-focus),var(--v2-elevation-raised)]": !props.open,
+            "shadow-[0_0_0_0.5px_var(--v2-border-border-focus)]": props.open,
           }}
         >
           <IconV2 name="magnifying-glass" />
@@ -1031,6 +1087,7 @@ function HomeSessionSearch(props: {
 
 function HomeSessionSearchResultRow(props: {
   record: HomeSessionRecord
+  showProjectName: boolean
   server: ServerConnection.Key
   activeServer: boolean
   selected: boolean
@@ -1038,6 +1095,7 @@ function HomeSessionSearchResultRow(props: {
   onSelect: (session: Session) => void
 }) {
   const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
+  const showProjectName = () => props.showProjectName && props.record.projectName
 
   const key = () => homeSessionSearchKey(props.record)
 
@@ -1064,11 +1122,11 @@ function HomeSessionSearchResultRow(props: {
       />
       <div class="flex min-w-0 flex-1 items-center gap-1.5">
         <span
-          class={`${HOME_SEARCH_RESULT_TITLE} ${props.record.projectName ? "max-w-[min(70%,480px)] flex-[0_1_auto]" : "flex-[1_1_auto]"}`}
+          class={`${HOME_SEARCH_RESULT_TITLE} ${showProjectName() ? "max-w-[min(70%,480px)] flex-[0_1_auto]" : "flex-[1_1_auto]"}`}
         >
           {title()}
         </span>
-        <Show when={props.record.projectName}>
+        <Show when={showProjectName()}>
           <span class={HOME_SEARCH_RESULT_META}>{props.record.projectName}</span>
         </Show>
       </div>
@@ -1079,7 +1137,7 @@ function HomeSessionSearchResultRow(props: {
 function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => void }) {
   const language = useLanguage()
   return (
-    <div class="flex h-7 min-w-0 items-center justify-between pl-4 pr-2">
+    <div class="flex h-7 min-w-0 items-center justify-between pl-[18px] pr-2">
       <div class={HOME_SECTION_LABEL}>{props.title}</div>
       <Show when={props.onNewSession}>
         {(onNewSession) => (
@@ -1101,36 +1159,58 @@ function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => voi
 
 function HomeSessionRow(props: {
   record: HomeSessionRecord
+  showProjectName: boolean
   server: ServerConnection.Key
   activeServer: boolean
-  onClick: () => void
+  openSession: (session: Session) => void
+  archiveSession: (session: Session) => Promise<void>
 }) {
+  const language = useLanguage()
   const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
+  const showProjectName = () => props.showProjectName && props.record.projectName
 
   return (
-    <button
-      type="button"
-      data-component="home-session-row"
-      class={`${HOME_ROW} h-10 gap-2 px-6 py-3 pl-4`}
-      onClick={props.onClick}
-    >
-      <HomeSessionLeading
-        project={props.record.project}
-        session={props.record.session}
-        server={props.server}
-        activeServer={props.activeServer}
-      />
-      <span
-        class={`min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-v2-text-text-base [font-weight:530] ${props.record.projectName ? "max-w-[min(70%,480px)] flex-[0_1_auto]" : "flex-[1_1_auto]"}`}
+    <div class="group/session relative flex h-10 min-w-0 items-center rounded-[6px]">
+      <button
+        type="button"
+        data-component="home-session-row"
+        class={`${HOME_ROW} h-10 min-w-0 flex-1 gap-2 py-3 pl-3 pr-10`}
+        onClick={() => props.openSession(props.record.session)}
       >
-        {title()}
-      </span>
-      <Show when={props.record.projectName}>
-        <span class="min-w-0 flex-[1_1_auto] overflow-hidden text-ellipsis whitespace-nowrap text-v2-text-text-muted [font-weight:440]">
-          {props.record.projectName}
+        <HomeSessionLeading
+          project={props.record.project}
+          session={props.record.session}
+          server={props.server}
+          activeServer={props.activeServer}
+        />
+        <span
+          class={`min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-v2-text-text-base [font-weight:530] ${showProjectName() ? "max-w-[min(70%,480px)] flex-[0_1_auto]" : "flex-[1_1_auto]"}`}
+        >
+          {title()}
         </span>
-      </Show>
-    </button>
+        <Show when={showProjectName()}>
+          <span class="min-w-0 flex-[1_1_auto] overflow-hidden text-ellipsis whitespace-nowrap text-v2-text-text-muted [font-weight:440]">
+            {props.record.projectName}
+          </span>
+        </Show>
+      </button>
+      <div class="absolute right-1.5 top-1/2 flex -translate-y-1/2 items-center gap-1 opacity-0 transition-opacity group-hover/session:opacity-100 focus-within:opacity-100">
+        <TooltipV2 class="flex shrink-0 items-center" placement="bottom" value={language.t("common.archive")}>
+          <IconButtonV2
+            data-action="home-session-archive"
+            variant="ghost-muted"
+            size="large"
+            icon={<IconV2 name="archive" />}
+            aria-label={language.t("common.archive")}
+            onClick={(event) => {
+              event.preventDefault()
+              event.stopPropagation()
+              void props.archiveSession(props.record.session)
+            }}
+          />
+        </TooltipV2>
+      </div>
+    </div>
   )
 }
 

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

@@ -130,8 +130,8 @@
   }
 
   &[data-size="x-large"] [data-slot="dialog-container"] {
-    width: min(calc(100vw - 32px), 960px);
-    height: min(calc(100vh - 32px), 600px);
+    width: min(calc(100vw - 32px), 980px);
+    height: min(calc(100vh - 92px), 600px);
   }
 }
 

+ 5 - 1
packages/ui/src/components/scroll-view.css

@@ -1,10 +1,14 @@
 .scroll-view {
   position: relative;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
   overflow: hidden;
 }
 
 .scroll-view__viewport {
-  height: 100%;
+  flex: 1 1 auto;
+  min-height: 0;
   width: 100%;
   overflow-y: auto;
   scrollbar-width: none;

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

@@ -416,7 +416,7 @@
       "v2-overlay-simple-overlay-pressed": "var(--v2-alpha-light-10)",
       "v2-overlay-simple-overlay-contrast-hover": "var(--v2-alpha-dark-24)",
       "v2-overlay-simple-overlay-contrast-pressed": "var(--v2-alpha-dark-40)",
-      "v2-overlay-simple-overlay-scrim": "var(--v2-alpha-light-30)",
+      "v2-overlay-simple-overlay-scrim": "var(--v2-alpha-dark-60)",
       "v2-overlay-gradient-depth-overlay-depth-top": "var(--v2-alpha-light-100)",
       "v2-overlay-gradient-depth-overlay-depth-bot": "var(--v2-alpha-light-0)",
       "v2-overlay-simple-tab-active-scrim": "#24242400",

+ 1 - 1
packages/ui/src/theme/v2/mapping.ts

@@ -90,7 +90,7 @@ const dark: Record<string, V2ColorValue> = {
   "v2-overlay-simple-overlay-pressed": ref("v2-alpha-light-10"),
   "v2-overlay-simple-overlay-contrast-hover": ref("v2-alpha-dark-24"),
   "v2-overlay-simple-overlay-contrast-pressed": ref("v2-alpha-dark-40"),
-  "v2-overlay-simple-overlay-scrim": ref("v2-alpha-light-30"),
+  "v2-overlay-simple-overlay-scrim": ref("v2-alpha-dark-60"),
   "v2-overlay-gradient-depth-overlay-depth-top": ref("v2-alpha-light-100"),
   "v2-overlay-gradient-depth-overlay-depth-bot": ref("v2-alpha-light-0"),
   "v2-overlay-simple-tab-active-scrim": "#24242400",

+ 2 - 2
packages/ui/src/v2/components/dialog-v2.css

@@ -144,7 +144,7 @@
   }
 
   &[data-size="x-large"] [data-slot="dialog-container"] {
-    width: 800px;
-    height: 560px;
+    width: min(calc(100vw - 32px), 980px);
+    height: min(calc(100vh - 92px), 600px);
   }
 }

+ 2 - 6
packages/ui/src/v2/components/icon-button-v2.css

@@ -112,9 +112,7 @@
   color: var(--v2-text-text-base);
 }
 
-[data-component="icon-button-v2"][data-variant="ghost"]:is(:hover, [data-state="hover"]):not(:disabled):not(
-    [data-expanded]
-  ) {
+[data-component="icon-button-v2"][data-variant="ghost"]:is(:hover, [data-state="hover"], [data-expanded]):not(:disabled) {
   background-color: var(--v2-overlay-simple-overlay-hover);
 }
 
@@ -133,9 +131,7 @@
   color: var(--v2-icon-icon-muted);
 }
 
-[data-component="icon-button-v2"][data-variant="ghost-muted"]:is(:hover, [data-state="hover"]):not(:disabled):not(
-    [data-expanded]
-  ) {
+[data-component="icon-button-v2"][data-variant="ghost-muted"]:is(:hover, [data-state="hover"], [data-expanded]):not(:disabled) {
   background-color: var(--v2-overlay-simple-overlay-hover);
 }
 

+ 4 - 0
packages/ui/src/v2/components/icon.tsx

@@ -65,6 +65,10 @@ const icons = {
     viewBox: "0 0 16 16",
     body: `<path d="M2.5 7.5H3.5V8.5H2.5V7.5Z" stroke="currentColor"/><path d="M7.5 7.5H8.5V8.5H7.5V7.5Z" stroke="currentColor"/><path d="M12.5 7.5H13.5V8.5H12.5V7.5Z" stroke="currentColor"/>`,
   },
+  archive: {
+    viewBox: "0 0 16 16",
+    body: `<path d="M13.1112 13.5555V14.0555H13.6112V13.5555H13.1112ZM2.889 13.5555H2.389L2.389 14.0555H2.889V13.5555ZM3.38901 5.55546L3.38901 5.05546L2.38901 5.05546L2.38901 5.55546L2.88901 5.55546L3.38901 5.55546ZM14.4446 2.44434H14.9446V1.94434L14.4446 1.94434L14.4446 2.44434ZM14.4446 5.55545L14.4446 6.05545L14.9446 6.05545V5.55545H14.4446ZM1.55566 5.55546L1.05566 5.55545L1.05566 6.05546L1.55566 6.05546L1.55566 5.55546ZM1.5557 2.44436L1.5557 1.94436L1.05571 1.94436L1.0557 2.44435L1.5557 2.44436ZM13.1112 5.55546H12.6112V13.5555H13.1112H13.6112V5.55546H13.1112ZM2.889 13.5555H3.389L3.38901 5.55546L2.88901 5.55546L2.38901 5.55546L2.389 13.5555H2.889ZM14.4446 2.44434H13.9446V5.55545H14.4446H14.9446V2.44434H14.4446ZM1.55566 5.55546L2.05566 5.55547L2.0557 2.44436L1.5557 2.44436L1.0557 2.44435L1.05566 5.55545L1.55566 5.55546ZM6.22234 8.22213V8.72213H9.7779V8.22213V7.72213H6.22234V8.22213ZM13.1112 13.5555V13.0555H2.889V13.5555V14.0555H13.1112V13.5555ZM1.5557 2.44436L1.5557 2.94436L14.4446 2.94434L14.4446 2.44434L14.4446 1.94434L1.5557 1.94436L1.5557 2.44436ZM14.4446 5.55545L14.4446 5.05545L1.55566 5.05546L1.55566 5.55546L1.55566 6.05546L14.4446 6.05545L14.4446 5.55545Z" fill="currentColor"/>`,
+  },
 }
 
 const spriteID = "opencode-v2-icon-sprite"

+ 2 - 2
packages/ui/src/v2/components/project-avatar-v2.css

@@ -120,7 +120,7 @@
   flex-shrink: 0;
   align-items: center;
   justify-content: center;
-  width: 22px;
-  height: 22px;
+  width: 16px;
+  height: 16px;
   overflow: visible;
 }

+ 1 - 1
packages/ui/src/v2/components/select-v2.css

@@ -134,7 +134,7 @@
   font-style: normal;
   font-weight: 440;
   font-size: 13px;
-  line-height: 1;
+  line-height: 16px;
   letter-spacing: -0.04px;
   color: var(--v2-text-text-base);
   font-variation-settings: "slnt" 0;

+ 3 - 1
packages/ui/src/v2/components/tabs-v2.css

@@ -45,6 +45,7 @@
   align-items: center;
   flex-shrink: 0;
   white-space: nowrap;
+  user-select: none;
 }
 
 [data-component="tabs-v2"] [data-slot="tabs-v2-trigger"] {
@@ -54,6 +55,7 @@
   overflow: hidden;
   text-overflow: ellipsis;
   outline: none;
+  user-select: none;
 
   font-size: 13px;
   font-weight: 440;
@@ -61,7 +63,7 @@
   letter-spacing: -0.04px;
 }
 
-[data-component="tabs-v2"] [data-slot="tabs-v2-trigger"]:focus-visible {
+[data-component="tabs-v2"] [data-slot="tabs-v2-trigger"]:is(:focus, :focus-visible) {
   outline: none;
   box-shadow: none;
 }

+ 1 - 1
packages/ui/src/v2/styles/theme.css

@@ -370,7 +370,7 @@
     --v2-overlay-simple-overlay-pressed: var(--v2-alpha-light-10);
     --v2-overlay-simple-overlay-contrast-hover: var(--v2-alpha-dark-24);
     --v2-overlay-simple-overlay-contrast-pressed: var(--v2-alpha-dark-40);
-    --v2-overlay-simple-overlay-scrim: var(--v2-alpha-light-30);
+    --v2-overlay-simple-overlay-scrim: var(--v2-alpha-dark-60);
     --v2-overlay-gradient-depth-overlay-depth-top: var(--v2-alpha-light-100);
     --v2-overlay-gradient-depth-overlay-depth-bot: var(--v2-alpha-light-0);
     --v2-overlay-simple-tab-active-scrim: #24242400;