Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 47 additions & 30 deletions packages/app/src/components/titlebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@ import {
Match,
onMount,
Show,
startTransition,
Switch,
untrack,
} from "solid-js"
import { createStore } from "solid-js/store"
import { useLocation, useMatch, useNavigate, useParams } from "@solidjs/router"
import { useLocation, useNavigate, useParams } from "@solidjs/router"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { Button } from "@opencode-ai/ui/button"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { useTheme } from "@opencode-ai/ui/theme/context"
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 { getProjectAvatarVariant, LayoutRoute, useLayout, type LocalProject } from "@/context/layout"
import { usePlatform } from "@/context/platform"
Expand Down Expand Up @@ -253,7 +254,6 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
{(_) => {
const serverSync = useServerSync()
const navigate = useNavigate()
const homeMatch = useMatch(() => "/")
const layout = useLayout()

const newSessionHref = () => {
Expand All @@ -268,17 +268,6 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
const tabs = useTabs()
const tabsStore = tabs.store
const tabsStoreActions = tabs
const navigateTab = (tab: Tab) => {
const href = tabHref(tab)
if (tab.server === server.key) {
navigate(href)
return
}
void startTransition(() => {
server.setActive(tab.server)
navigate(href)
})
}

const matchRoute = (route: LayoutRoute) => {
if (route.type === "home") return
Expand Down Expand Up @@ -309,7 +298,10 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
const route = layout.route()
if (!tabs.ready()) return
const tab = currentTab()
if (tab) return
if (tab) {
tabs.remember(tab)
return
}

if (route.type === "session") {
const sync = serverSync.createDirSyncContext(route.dir)
Expand All @@ -332,6 +324,18 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
})

const openNewTab = () => navigate(newSessionHref())
const toggleHome = () => tabs.toggleHome({ home: layout.route().type === "home", current: currentTab() })

command.register("titlebar-home", () => [
{
id: "home.toggle",
title: language.t("home.title"),
category: language.t("command.category.view"),
keybind: "mod+b",
hidden: true,
onSelect: toggleHome,
},
])

command.register("tabs", () => {
const current = currentTab()
Expand Down Expand Up @@ -369,7 +373,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
if (index === -1) index = tabsStore.length - 1

const next = tabsStore[index]
if (next) navigateTab(next)
if (next) tabs.select(next)
},
},
{
Expand All @@ -386,7 +390,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
if (index === tabsStore.length) index = 0

const next = tabsStore[index]
if (next) navigateTab(next)
if (next) tabs.select(next)
},
},
...Array.from({ length: 9 }, (_, i) => {
Expand All @@ -401,7 +405,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
hidden: true,
onSelect: () => {
const tab = tabsStore[index]
if (tab) navigateTab(tab)
if (tab) tabs.select(tab)
},
}
}),
Expand All @@ -427,15 +431,28 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
<Show when={windows() || linux()}>
<WindowsAppMenu command={command} platform={platform} variant="v2" />
</Show>
<IconButtonV2
variant="ghost-muted"
size="large"
as="a"
href="/"
class="!w-9 shrink-0"
icon={<IconV2 name="grid-plus" />}
state={!!homeMatch() ? "pressed" : undefined}
/>
<TooltipV2
placement="bottom"
value={
<>
{language.t("home.title")}
<KeybindV2 keys={command.keybindParts("home.toggle")} variant="neutral" />
</>
}
class="shrink-0"
>
<IconButtonV2
type="button"
variant="ghost-muted"
size="large"
class="!w-9 shrink-0"
icon={<IconV2 name="grid-plus" />}
state={layout.route().type === "home" ? "pressed" : undefined}
onClick={toggleHome}
aria-label={language.t("home.title")}
aria-pressed={layout.route().type === "home"}
/>
</TooltipV2>

<div data-slot="titlebar-tabs" class="relative min-w-0">
<div
Expand Down Expand Up @@ -469,7 +486,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
title={language.t("command.session.new")}
active={currentTab() === tab}
onNavigate={() => {
navigateTab(tab)
tabs.select(tab)
ref.scrollIntoView({ behavior: "instant" })
}}
onClose={() => tabsStoreActions.removeTab(i())}
Expand All @@ -488,7 +505,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
directory={decode64(tab.dirBase64)!}
sessionId={tab.sessionId}
onNavigate={() => {
navigateTab(tab)
tabs.select(tab)

ref.scrollIntoView({ behavior: "instant" })
}}
Expand Down Expand Up @@ -518,7 +535,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
title={language.t("command.session.new")}
onClose={() => {
const tab = tabsStore.at(-1)
if (tab) navigateTab(tab)
if (tab) tabs.select(tab)
else navigate("/")
}}
/>
Expand Down
105 changes: 56 additions & 49 deletions packages/app/src/context/command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,54 +170,60 @@ export function matchKeybind(keybinds: Keybind[], event: KeyboardEvent): boolean
return false
}

export function formatKeybind(config: string, t?: (key: KeyLabel) => string): string {
if (!config || config === "none") return ""

const keybinds = parseKeybind(config)
if (keybinds.length === 0) return ""

const kb = keybinds[0]
function displayKeybindParts(kb: Keybind, t?: (key: KeyLabel) => string) {
const parts: string[] = []

if (kb.ctrl) parts.push(IS_MAC ? "⌃" : keyText("common.key.ctrl", t))
if (kb.alt) parts.push(IS_MAC ? "⌥" : keyText("common.key.alt", t))
if (kb.shift) parts.push(IS_MAC ? "⇧" : keyText("common.key.shift", t))
if (kb.meta) parts.push(IS_MAC ? "⌘" : keyText("common.key.meta", t))

if (kb.key) {
const keys: Record<string, string> = {
arrowup: "↑",
arrowdown: "↓",
arrowleft: "←",
arrowright: "→",
comma: ",",
plus: "+",
}
const named: Record<string, KeyLabel> = {
backspace: "common.key.backspace",
delete: "common.key.delete",
end: "common.key.end",
enter: "common.key.enter",
esc: "common.key.esc",
escape: "common.key.esc",
home: "common.key.home",
insert: "common.key.insert",
pagedown: "common.key.pageDown",
pageup: "common.key.pageUp",
space: "common.key.space",
tab: "common.key.tab",
}
const key = kb.key.toLowerCase()
const displayKey =
keys[key] ??
(named[key]
? keyText(named[key], t)
: key.length === 1
? key.toUpperCase()
: key.charAt(0).toUpperCase() + key.slice(1))
parts.push(displayKey)
if (!kb.key) return parts

const keys: Record<string, string> = {
arrowup: "↑",
arrowdown: "↓",
arrowleft: "←",
arrowright: "→",
comma: ",",
plus: "+",
}
const named: Record<string, KeyLabel> = {
backspace: "common.key.backspace",
delete: "common.key.delete",
end: "common.key.end",
enter: "common.key.enter",
esc: "common.key.esc",
escape: "common.key.esc",
home: "common.key.home",
insert: "common.key.insert",
pagedown: "common.key.pageDown",
pageup: "common.key.pageUp",
space: "common.key.space",
tab: "common.key.tab",
}
const key = kb.key.toLowerCase()
const displayKey =
keys[key] ??
(named[key]
? keyText(named[key], t)
: key.length === 1
? key.toUpperCase()
: key.charAt(0).toUpperCase() + key.slice(1))
parts.push(displayKey)

return parts
}

export function formatKeybindParts(config: string, t?: (key: KeyLabel) => string): string[] {
if (!config || config === "none") return []
const keybind = parseKeybind(config)[0]
return keybind ? displayKeybindParts(keybind, t) : []
}

export function formatKeybind(config: string, t?: (key: KeyLabel) => string): string {
const parts = formatKeybindParts(config, t)
if (parts.length === 0) return ""
return IS_MAC ? parts.join("") : parts.join("+")
}

Expand Down Expand Up @@ -402,25 +408,26 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
})
}

const keybindConfig = (id: string) => {
if (id === PALETTE_ID) return settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND
const base = actionId(id)
return options().find((x) => actionId(x.id) === base)?.keybind ?? bind(base, catalog[base]?.keybind)
}

return {
register,
trigger(id: string, source?: CommandSource) {
run(id, source)
},
keybind(id: string) {
if (id === PALETTE_ID) {
return formatKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND, language.t)
}

const base = actionId(id)
const option = options().find((x) => actionId(x.id) === base)
if (option?.keybind) return formatKeybind(option.keybind, language.t)

const meta = catalog[base]
const config = bind(base, meta?.keybind)
const config = keybindConfig(id)
if (!config) return ""
return formatKeybind(config, language.t)
},
keybindParts(id: string) {
const config = keybindConfig(id)
return config ? formatKeybindParts(config, language.t) : []
},
show: showPalette,
keybinds(enabled: boolean) {
setStore("suspendCount", (count) => Math.max(0, count + (enabled ? -1 : 1)))
Expand Down
Loading
Loading