Skip to content

Commit 3dde5d1

Browse files
committed
feat(app): ui improvements
Reapply UI polish on top of upstream dev: home session archive, scoped search, titlebar tab focus and tooltips, review panel keybind tooltip, theme and component CSS tweaks.
1 parent 5152150 commit 3dde5d1

21 files changed

Lines changed: 270 additions & 126 deletions

File tree

packages/app/src/components/prompt-input.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2210,7 +2210,9 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) {
22102210
)}
22112211
</Show>
22122212
<span class="truncate">{props.state.modelName}</span>
2213-
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
2213+
<span class="-ml-1 shrink-0 flex size-fit">
2214+
<Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" />
2215+
</span>
22142216
</Button>
22152217
</TooltipKeybind>
22162218
}
@@ -2239,7 +2241,9 @@ function ComposerModelControl(props: { state: ComposerModelControlState }) {
22392241
)}
22402242
</Show>
22412243
<span class="truncate">{props.state.modelName}</span>
2242-
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
2244+
<span class="-ml-1 shrink-0 flex size-fit">
2245+
<Icon name="chevron-down" size="small" class="text-v2-icon-icon-muted" />
2246+
</span>
22432247
</ModelSelectorPopover>
22442248
</TooltipKeybind>
22452249
</Show>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const ServerRowMenu: Component<{
1919
const isDefault = () => props.controller.defaultKey() === key
2020

2121
return (
22-
<MenuV2 gutter={4} modal={false} placement="bottom-end" open={props.open} onOpenChange={props.onOpenChange}>
22+
<MenuV2 gutter={6} modal={false} placement="bottom-end" open={props.open} onOpenChange={props.onOpenChange}>
2323
<MenuV2.Trigger
2424
as={IconButtonV2}
2525
variant="ghost-muted"

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export function ServerHealthIndicator(props: { health?: ServerHealth }) {
117117
return (
118118
<div
119119
classList={{
120-
"size-1.5 rounded-full shrink-0": true,
120+
"size-1.5 rounded-full shrink-0 my-[3.5px]": true,
121121
"bg-icon-success-base": props.health?.healthy === true,
122122
"bg-icon-critical-base": props.health?.healthy === false,
123123
"bg-border-weak-base": props.health === undefined,

packages/app/src/components/session/session-header.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { createEffect, createMemo, createSignal, For, onMount, Show } from "soli
1212
import { createStore } from "solid-js/store"
1313
import { createMediaQuery } from "@solid-primitives/media"
1414
import { Portal } from "solid-js/web"
15-
import { useCommand } from "@/context/command"
15+
import { formatKeybindKeys, useCommand } from "@/context/command"
1616
import { useLanguage } from "@/context/language"
1717
import { useLayout } from "@/context/layout"
1818
import { usePlatform } from "@/context/platform"
@@ -28,6 +28,8 @@ import { Persist, persisted } from "@/utils/persist"
2828
import { StatusPopover, StatusPopoverV2 } from "../status-popover"
2929
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
3030
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
31+
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
32+
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
3133

3234
const OPEN_APPS = [
3335
"vscode",
@@ -527,6 +529,8 @@ type SessionHeaderV2ActionsState = {
527529
}
528530

529531
function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
532+
const language = useLanguage()
533+
530534
return (
531535
<div class="flex items-center gap-2">
532536
<Show when={props.state.statusVisible}>
@@ -535,7 +539,17 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
535539
</Tooltip>
536540
</Show>
537541
<Show when={props.state.reviewVisible}>
538-
<TooltipKeybind title={props.state.reviewLabel} keybind={props.state.reviewKeybind}>
542+
<TooltipV2
543+
placement="bottom"
544+
value={
545+
<>
546+
{props.state.reviewLabel}
547+
<Show when={props.state.reviewKeybind}>
548+
<KeybindV2 keys={formatKeybindKeys(props.state.reviewKeybind, language.t)} variant="neutral" />
549+
</Show>
550+
</>
551+
}
552+
>
539553
<IconButtonV2
540554
type="button"
541555
variant="ghost-muted"
@@ -548,7 +562,7 @@ function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
548562
aria-controls="review-panel"
549563
icon={<IconV2 name="sidebar-right" />}
550564
/>
551-
</TooltipKeybind>
565+
</TooltipV2>
552566
</Show>
553567
</div>
554568
)

packages/app/src/components/titlebar.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,29 @@
1+
[data-slot="titlebar-tab-item"] {
2+
user-select: none;
3+
}
4+
5+
[data-slot="titlebar-tab-item"] a {
6+
outline: none;
7+
}
8+
9+
[data-slot="titlebar-v2"] [data-component="icon-button-v2"][data-variant="ghost-muted"]:is(:focus-visible, [data-state="focus"]):not(:disabled) {
10+
outline: none;
11+
background-color: var(--v2-overlay-simple-overlay-hover);
12+
}
13+
14+
[data-slot="titlebar-tab-item"] [data-component="icon-button-v2"]:is(:focus-visible, [data-state="focus"]):not(:disabled) {
15+
opacity: 1;
16+
}
17+
18+
[data-slot="titlebar-tab-item"]:has([data-component="icon-button-v2"]:focus-visible) [data-slot="titlebar-tab-close"] {
19+
right: 0;
20+
left: auto;
21+
}
22+
23+
[data-slot="titlebar-tab-item"]:has([data-component="icon-button-v2"]:focus-visible) [data-slot="titlebar-tab-close-fade"] {
24+
background-image: var(--active-bg);
25+
}
26+
127
@keyframes titlebar-tab-fade-left {
228
from {
329
visibility: hidden;

packages/app/src/components/titlebar.tsx

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
2424

2525
import { LayoutRoute, useLayout } from "@/context/layout"
2626
import { usePlatform } from "@/context/platform"
27-
import { useCommand } from "@/context/command"
27+
import { formatKeybindKeys, useCommand } from "@/context/command"
2828
import { useLanguage } from "@/context/language"
2929
import { useSettings } from "@/context/settings"
3030
import { WindowsAppMenu } from "./windows-app-menu"
@@ -236,6 +236,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
236236

237237
return (
238238
<header
239+
data-slot={useV2Titlebar() ? "titlebar-v2" : undefined}
239240
classList={{
240241
"shrink-0 relative flex flex-row": true,
241242
"h-9 bg-v2-background-bg-deep overflow-visible": useV2Titlebar(),
@@ -351,6 +352,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
351352
tabs.newDraft({ server: fallback.server, directory: fallback.project.worktree }, "")
352353
}
353354
const toggleHome = () => tabs.toggleHome({ home: layout.route().type === "home", current: currentTab() })
355+
const newTabKeybind = "mod+t"
354356

355357
command.register("titlebar-home", () => [
356358
{
@@ -371,7 +373,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
371373
id: "tab.new",
372374
category: "tab",
373375
title: language.t("command.session.new"),
374-
keybind: "mod+t",
376+
keybind: newTabKeybind,
375377
hidden: true,
376378
onSelect: openNewTab,
377379
},
@@ -604,15 +606,25 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
604606
/>
605607
</div>
606608
<Show when={!(creating() && params.dir)}>
607-
<IconButtonV2
608-
type="button"
609-
variant="ghost-muted"
610-
size="large"
611-
class="shrink-0"
612-
icon={<IconV2 name="plus" />}
613-
onClick={openNewTab}
614-
aria-label={language.t("command.session.new")}
615-
/>
609+
<TooltipV2
610+
placement="bottom"
611+
value={
612+
<>
613+
{language.t("command.session.new")}
614+
<KeybindV2 keys={formatKeybindKeys(newTabKeybind, language.t)} variant="neutral" />
615+
</>
616+
}
617+
>
618+
<IconButtonV2
619+
type="button"
620+
variant="ghost-muted"
621+
size="large"
622+
class="shrink-0"
623+
icon={<IconV2 name="plus" />}
624+
onClick={openNewTab}
625+
aria-label={language.t("command.session.new")}
626+
/>
627+
</TooltipV2>
616628
</Show>
617629
<div class="flex-1" />
618630
<TitlebarV2Right state={v2RightState()} />
@@ -808,16 +820,16 @@ function TitlebarV2Right(props: { state: TitlebarV2RightState }) {
808820

809821
function TitlebarUpdateIconButton(props: { state: TitlebarUpdatePillState }) {
810822
return (
811-
<div class="relative isolate mr-3 size-5 shrink-0">
823+
<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">
812824
<button
813825
type="button"
814-
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"
826+
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"
815827
onClick={props.state.onInstall}
816828
disabled={props.state.installing}
817829
aria-busy={props.state.installing}
818830
aria-label={props.state.ariaLabel}
819831
>
820-
<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">
832+
<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">
821833
Update
822834
</span>
823835
<span class="flex size-5 shrink-0 items-center justify-center">
@@ -863,7 +875,8 @@ function TabNavItem(props: {
863875
return (
864876
<div
865877
ref={props.ref}
866-
class="group relative flex h-7 min-w-24 max-w-60 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)]"
878+
data-slot="titlebar-tab-item"
879+
class="group relative flex h-7 min-w-24 max-w-60 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)]"
867880
data-active={props.active}
868881
onMouseDown={(event) => {
869882
if (event.button !== 1) return
@@ -898,10 +911,12 @@ function TabNavItem(props: {
898911
</Show>
899912

900913
<div
914+
data-slot="titlebar-tab-close"
901915
class="absolute not-group-hover:not-group-data-[active=true]:not-data-[truncate=true]:left-52 group-hover:right-0 group-data-[active=true]:right-0 data-[truncate=true]:right-0 inset-y-0 flex flex-row items-center pr-1 py-1 w-8 pl-2"
902916
data-truncate={props.forceTruncate}
903917
>
904918
<div
919+
data-slot="titlebar-tab-close-fade"
905920
class="absolute inset-0 rounded-r-[6px] bg-(image:--inactive-bg) group-hover:bg-(image:--active-bg) group-data-[active=true]:bg-(image:--active-bg)"
906921
style={{
907922
"--inactive-bg": "linear-gradient(to right, transparent 0%, var(--tab-bg) 80%)",
@@ -936,8 +951,9 @@ function DraftTabItem(props: {
936951
return (
937952
<div
938953
ref={props.ref}
954+
data-slot="titlebar-tab-item"
939955
data-active={props.active}
940-
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)] focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-[var(--v2-border-border-focus)]"
956+
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)]"
941957
onMouseDown={(event) => {
942958
if (event.button !== 1) return
943959
closeTab(event)
@@ -982,7 +998,8 @@ function NewSessionTabItem(props: { ref?: HTMLDivElement; href: string; title: s
982998
return (
983999
<div
9841000
ref={props.ref}
985-
class="group relative shrink-0 flex h-7 max-w-60 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--v2-overlay-simple-overlay-pressed)] pl-1.5 pr-8 whitespace-nowrap focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-[var(--v2-border-border-focus)]"
1001+
data-slot="titlebar-tab-item"
1002+
class="group relative shrink-0 flex h-7 max-w-60 flex-row items-center gap-1.5 overflow-hidden rounded-[6px] bg-[var(--v2-overlay-simple-overlay-pressed)] has-[>a:focus-visible]:bg-[var(--v2-background-bg-layer-02)] pl-1.5 pr-8 whitespace-nowrap"
9861003
onMouseDown={(event) => {
9871004
if (event.button !== 1) return
9881005
closeTab(event)

packages/app/src/context/command.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,11 @@ export function formatKeybind(config: string, t?: (key: KeyLabel) => string): st
227227
return IS_MAC ? parts.join("") : parts.join("+")
228228
}
229229

230+
// KeybindV2 takes an array instead of a string
231+
export function formatKeybindKeys(config: string, t?: (key: KeyLabel) => string): string[] {
232+
return formatKeybindParts(config, t)
233+
}
234+
230235
function isEditableTarget(target: EventTarget | null) {
231236
if (!(target instanceof HTMLElement)) return false
232237
if (target.isContentEditable) return true

packages/app/src/i18n/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,7 @@ export const dict = {
592592
"home.server.collapse": "Collapse server projects",
593593
"home.server.expand": "Expand server projects",
594594
"home.sessions.search.placeholder": "Search sessions",
595+
"home.sessions.search.placeholder.scoped": "Search sessions in {{scope}}",
595596
"home.sessions.search.sessions": "Sessions",
596597
"home.sessions.search.noResults": "No sessions found for {{query}}",
597598
"home.sessions.empty": "No sessions found",

packages/app/src/i18n/zh.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,7 @@ export const dict = {
507507
"home.projects": "项目",
508508
"home.project.add": "添加项目",
509509
"home.sessions.search.placeholder": "搜索会话",
510+
"home.sessions.search.placeholder.scoped": "在 {{scope}} 中搜索会话",
510511
"home.sessions.search.sessions": "会话",
511512
"home.sessions.search.noResults": "未找到与 {{query}} 相关的会话",
512513
"home.sessions.empty": "未找到会话",

0 commit comments

Comments
 (0)