Skip to content

Commit 5c9e4ff

Browse files
authored
feat(app): add v2 home tab toggle (anomalyco#32191)
1 parent 417ad24 commit 5c9e4ff

6 files changed

Lines changed: 168 additions & 87 deletions

File tree

packages/app/src/components/titlebar.tsx

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,20 @@ import {
77
Match,
88
onMount,
99
Show,
10-
startTransition,
1110
Switch,
1211
untrack,
1312
} from "solid-js"
1413
import { createStore } from "solid-js/store"
15-
import { useLocation, useMatch, useNavigate, useParams } from "@solidjs/router"
14+
import { useLocation, useNavigate, useParams } from "@solidjs/router"
1615
import { IconButton } from "@opencode-ai/ui/icon-button"
1716
import { Icon } from "@opencode-ai/ui/icon"
1817
import { Button } from "@opencode-ai/ui/button"
1918
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
2019
import { useTheme } from "@opencode-ai/ui/theme/context"
2120
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
2221
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
22+
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
23+
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
2324

2425
import { getProjectAvatarVariant, LayoutRoute, useLayout, type LocalProject } from "@/context/layout"
2526
import { usePlatform } from "@/context/platform"
@@ -253,7 +254,6 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
253254
{(_) => {
254255
const serverSync = useServerSync()
255256
const navigate = useNavigate()
256-
const homeMatch = useMatch(() => "/")
257257
const layout = useLayout()
258258

259259
const newSessionHref = () => {
@@ -268,17 +268,6 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
268268
const tabs = useTabs()
269269
const tabsStore = tabs.store
270270
const tabsStoreActions = tabs
271-
const navigateTab = (tab: Tab) => {
272-
const href = tabHref(tab)
273-
if (tab.server === server.key) {
274-
navigate(href)
275-
return
276-
}
277-
void startTransition(() => {
278-
server.setActive(tab.server)
279-
navigate(href)
280-
})
281-
}
282271

283272
const matchRoute = (route: LayoutRoute) => {
284273
if (route.type === "home") return
@@ -309,7 +298,10 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
309298
const route = layout.route()
310299
if (!tabs.ready()) return
311300
const tab = currentTab()
312-
if (tab) return
301+
if (tab) {
302+
tabs.remember(tab)
303+
return
304+
}
313305

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

334326
const openNewTab = () => navigate(newSessionHref())
327+
const toggleHome = () => tabs.toggleHome({ home: layout.route().type === "home", current: currentTab() })
328+
329+
command.register("titlebar-home", () => [
330+
{
331+
id: "home.toggle",
332+
title: language.t("home.title"),
333+
category: language.t("command.category.view"),
334+
keybind: "mod+b",
335+
hidden: true,
336+
onSelect: toggleHome,
337+
},
338+
])
335339

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

371375
const next = tabsStore[index]
372-
if (next) navigateTab(next)
376+
if (next) tabs.select(next)
373377
},
374378
},
375379
{
@@ -386,7 +390,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
386390
if (index === tabsStore.length) index = 0
387391

388392
const next = tabsStore[index]
389-
if (next) navigateTab(next)
393+
if (next) tabs.select(next)
390394
},
391395
},
392396
...Array.from({ length: 9 }, (_, i) => {
@@ -401,7 +405,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
401405
hidden: true,
402406
onSelect: () => {
403407
const tab = tabsStore[index]
404-
if (tab) navigateTab(tab)
408+
if (tab) tabs.select(tab)
405409
},
406410
}
407411
}),
@@ -427,15 +431,28 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
427431
<Show when={windows() || linux()}>
428432
<WindowsAppMenu command={command} platform={platform} variant="v2" />
429433
</Show>
430-
<IconButtonV2
431-
variant="ghost-muted"
432-
size="large"
433-
as="a"
434-
href="/"
435-
class="!w-9 shrink-0"
436-
icon={<IconV2 name="grid-plus" />}
437-
state={!!homeMatch() ? "pressed" : undefined}
438-
/>
434+
<TooltipV2
435+
placement="bottom"
436+
value={
437+
<>
438+
{language.t("home.title")}
439+
<KeybindV2 keys={command.keybindParts("home.toggle")} variant="neutral" />
440+
</>
441+
}
442+
class="shrink-0"
443+
>
444+
<IconButtonV2
445+
type="button"
446+
variant="ghost-muted"
447+
size="large"
448+
class="!w-9 shrink-0"
449+
icon={<IconV2 name="grid-plus" />}
450+
state={layout.route().type === "home" ? "pressed" : undefined}
451+
onClick={toggleHome}
452+
aria-label={language.t("home.title")}
453+
aria-pressed={layout.route().type === "home"}
454+
/>
455+
</TooltipV2>
439456

440457
<div data-slot="titlebar-tabs" class="relative min-w-0">
441458
<div
@@ -469,7 +486,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
469486
title={language.t("command.session.new")}
470487
active={currentTab() === tab}
471488
onNavigate={() => {
472-
navigateTab(tab)
489+
tabs.select(tab)
473490
ref.scrollIntoView({ behavior: "instant" })
474491
}}
475492
onClose={() => tabsStoreActions.removeTab(i())}
@@ -488,7 +505,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
488505
directory={decode64(tab.dirBase64)!}
489506
sessionId={tab.sessionId}
490507
onNavigate={() => {
491-
navigateTab(tab)
508+
tabs.select(tab)
492509

493510
ref.scrollIntoView({ behavior: "instant" })
494511
}}
@@ -518,7 +535,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
518535
title={language.t("command.session.new")}
519536
onClose={() => {
520537
const tab = tabsStore.at(-1)
521-
if (tab) navigateTab(tab)
538+
if (tab) tabs.select(tab)
522539
else navigate("/")
523540
}}
524541
/>

packages/app/src/context/command.tsx

Lines changed: 56 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -170,54 +170,60 @@ export function matchKeybind(keybinds: Keybind[], event: KeyboardEvent): boolean
170170
return false
171171
}
172172

173-
export function formatKeybind(config: string, t?: (key: KeyLabel) => string): string {
174-
if (!config || config === "none") return ""
175-
176-
const keybinds = parseKeybind(config)
177-
if (keybinds.length === 0) return ""
178-
179-
const kb = keybinds[0]
173+
function displayKeybindParts(kb: Keybind, t?: (key: KeyLabel) => string) {
180174
const parts: string[] = []
181175

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

187-
if (kb.key) {
188-
const keys: Record<string, string> = {
189-
arrowup: "↑",
190-
arrowdown: "↓",
191-
arrowleft: "←",
192-
arrowright: "→",
193-
comma: ",",
194-
plus: "+",
195-
}
196-
const named: Record<string, KeyLabel> = {
197-
backspace: "common.key.backspace",
198-
delete: "common.key.delete",
199-
end: "common.key.end",
200-
enter: "common.key.enter",
201-
esc: "common.key.esc",
202-
escape: "common.key.esc",
203-
home: "common.key.home",
204-
insert: "common.key.insert",
205-
pagedown: "common.key.pageDown",
206-
pageup: "common.key.pageUp",
207-
space: "common.key.space",
208-
tab: "common.key.tab",
209-
}
210-
const key = kb.key.toLowerCase()
211-
const displayKey =
212-
keys[key] ??
213-
(named[key]
214-
? keyText(named[key], t)
215-
: key.length === 1
216-
? key.toUpperCase()
217-
: key.charAt(0).toUpperCase() + key.slice(1))
218-
parts.push(displayKey)
181+
if (!kb.key) return parts
182+
183+
const keys: Record<string, string> = {
184+
arrowup: "↑",
185+
arrowdown: "↓",
186+
arrowleft: "←",
187+
arrowright: "→",
188+
comma: ",",
189+
plus: "+",
190+
}
191+
const named: Record<string, KeyLabel> = {
192+
backspace: "common.key.backspace",
193+
delete: "common.key.delete",
194+
end: "common.key.end",
195+
enter: "common.key.enter",
196+
esc: "common.key.esc",
197+
escape: "common.key.esc",
198+
home: "common.key.home",
199+
insert: "common.key.insert",
200+
pagedown: "common.key.pageDown",
201+
pageup: "common.key.pageUp",
202+
space: "common.key.space",
203+
tab: "common.key.tab",
219204
}
205+
const key = kb.key.toLowerCase()
206+
const displayKey =
207+
keys[key] ??
208+
(named[key]
209+
? keyText(named[key], t)
210+
: key.length === 1
211+
? key.toUpperCase()
212+
: key.charAt(0).toUpperCase() + key.slice(1))
213+
parts.push(displayKey)
214+
215+
return parts
216+
}
217+
218+
export function formatKeybindParts(config: string, t?: (key: KeyLabel) => string): string[] {
219+
if (!config || config === "none") return []
220+
const keybind = parseKeybind(config)[0]
221+
return keybind ? displayKeybindParts(keybind, t) : []
222+
}
220223

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

@@ -402,25 +408,26 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
402408
})
403409
}
404410

411+
const keybindConfig = (id: string) => {
412+
if (id === PALETTE_ID) return settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND
413+
const base = actionId(id)
414+
return options().find((x) => actionId(x.id) === base)?.keybind ?? bind(base, catalog[base]?.keybind)
415+
}
416+
405417
return {
406418
register,
407419
trigger(id: string, source?: CommandSource) {
408420
run(id, source)
409421
},
410422
keybind(id: string) {
411-
if (id === PALETTE_ID) {
412-
return formatKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND, language.t)
413-
}
414-
415-
const base = actionId(id)
416-
const option = options().find((x) => actionId(x.id) === base)
417-
if (option?.keybind) return formatKeybind(option.keybind, language.t)
418-
419-
const meta = catalog[base]
420-
const config = bind(base, meta?.keybind)
423+
const config = keybindConfig(id)
421424
if (!config) return ""
422425
return formatKeybind(config, language.t)
423426
},
427+
keybindParts(id: string) {
428+
const config = keybindConfig(id)
429+
return config ? formatKeybindParts(config, language.t) : []
430+
},
424431
show: showPalette,
425432
keybinds(enabled: boolean) {
426433
setStore("suspendCount", (count) => Math.max(0, count + (enabled ? -1 : 1)))

0 commit comments

Comments
 (0)