feat(app): forward and back buttons

This commit is contained in:
adamelmore
2026-01-26 22:58:29 -06:00
parent c0a5f85349
commit 58b9b54600
6 changed files with 130 additions and 27 deletions

View File

@@ -1,8 +1,10 @@
import { createEffect, createMemo, Show } from "solid-js"
import { createEffect, createMemo, Show, untrack } from "solid-js"
import { createStore } from "solid-js/store"
import { useLocation, useNavigate } 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 { TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { useTheme } from "@opencode-ai/ui/theme"
import { useLayout } from "@/context/layout"
@@ -16,11 +18,68 @@ export function Titlebar() {
const command = useCommand()
const language = useLanguage()
const theme = useTheme()
const navigate = useNavigate()
const location = useLocation()
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
const web = createMemo(() => platform.platform === "web")
const [history, setHistory] = createStore({
stack: [] as string[],
index: 0,
action: undefined as "back" | "forward" | undefined,
})
const path = () => `${location.pathname}${location.search}${location.hash}`
createEffect(() => {
const current = path()
untrack(() => {
if (!history.stack.length) {
const stack = current === "/" ? ["/"] : ["/", current]
setHistory({ stack, index: stack.length - 1 })
return
}
const active = history.stack[history.index]
if (current === active) {
if (history.action) setHistory("action", undefined)
return
}
if (history.action) {
setHistory("action", undefined)
return
}
const next = history.stack.slice(0, history.index + 1).concat(current)
setHistory({ stack: next, index: next.length - 1 })
})
})
const canBack = createMemo(() => history.index > 0)
const canForward = createMemo(() => history.index < history.stack.length - 1)
const back = () => {
if (!canBack()) return
const index = history.index - 1
const to = history.stack[index]
if (!to) return
setHistory({ index, action: "back" })
navigate(to)
}
const forward = () => {
if (!canForward()) return
const index = history.index + 1
const to = history.stack[index]
if (!to) return
setHistory({ index, action: "forward" })
navigate(to)
}
const getWin = () => {
if (platform.platform !== "desktop") return
@@ -106,34 +165,56 @@ export function Titlebar() {
/>
</div>
</Show>
<TooltipKeybind
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
placement="bottom"
title={language.t("command.sidebar.toggle")}
keybind={command.keybind("sidebar.toggle")}
>
<Button
variant="ghost"
class="group/sidebar-toggle size-6 p-0"
onClick={layout.sidebar.toggle}
aria-label={language.t("command.sidebar.toggle")}
aria-expanded={layout.sidebar.opened()}
<div class="flex items-center gap-1 shrink-0">
<TooltipKeybind
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
placement="bottom"
title={language.t("command.sidebar.toggle")}
keybind={command.keybind("sidebar.toggle")}
>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.sidebar.opened() ? "layout-left-full" : "layout-left"}
class="group-hover/sidebar-toggle:hidden"
<Button
variant="ghost"
class="group/sidebar-toggle size-6 p-0"
onClick={layout.sidebar.toggle}
aria-label={language.t("command.sidebar.toggle")}
aria-expanded={layout.sidebar.opened()}
>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.sidebar.opened() ? "layout-left-full" : "layout-left"}
class="group-hover/sidebar-toggle:hidden"
/>
<Icon size="small" name="layout-left-partial" class="hidden group-hover/sidebar-toggle:inline-block" />
<Icon
size="small"
name={layout.sidebar.opened() ? "layout-left" : "layout-left-full"}
class="hidden group-active/sidebar-toggle:inline-block"
/>
</div>
</Button>
</TooltipKeybind>
<div class="hidden xl:flex items-center gap-1 shrink-0">
<Tooltip placement="bottom" value={language.t("common.goBack")}>
<IconButton
icon="arrow-left"
variant="ghost"
disabled={!canBack()}
onClick={back}
aria-label={language.t("common.goBack")}
/>
<Icon size="small" name="layout-left-partial" class="hidden group-hover/sidebar-toggle:inline-block" />
<Icon
size="small"
name={layout.sidebar.opened() ? "layout-left" : "layout-left-full"}
class="hidden group-active/sidebar-toggle:inline-block"
</Tooltip>
<Tooltip placement="bottom" value={language.t("common.goForward")}>
<IconButton
icon="arrow-right"
variant="ghost"
disabled={!canForward()}
onClick={forward}
aria-label={language.t("common.goForward")}
/>
</div>
</Button>
</TooltipKeybind>
</Tooltip>
</div>
</div>
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" data-tauri-drag-region />
<div class="flex-1 h-full" data-tauri-drag-region />
<div

View File

@@ -17,6 +17,12 @@ export type Platform = {
/** Restart the app */
restart(): Promise<void>
/** Navigate back in history */
back(): void
/** Navigate forward in history */
forward(): void
/** Send a system notification (optional deep link) */
notify(title: string, description?: string, href?: string): Promise<void>

View File

@@ -31,6 +31,12 @@ const platform: Platform = {
openLink(url: string) {
window.open(url, "_blank")
},
back() {
window.history.back()
},
forward() {
window.history.forward()
},
restart: async () => {
window.location.reload()
},

View File

@@ -167,6 +167,7 @@ export const dict = {
"common.search.placeholder": "Search",
"common.goBack": "Go back",
"common.goForward": "Go forward",
"common.loading": "Loading",
"common.loading.ellipsis": "...",
"common.cancel": "Cancel",

View File

@@ -80,6 +80,14 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
void shellOpen(url).catch(() => undefined)
},
back() {
window.history.back()
},
forward() {
window.history.forward()
},
storage: (() => {
type StoreLike = {
get(key: string): Promise<string | null | undefined>

View File

@@ -4,6 +4,7 @@ const icons = {
"align-right": `<path d="M12.292 6.04167L16.2503 9.99998L12.292 13.9583M2.91699 9.99998H15.6253M17.0837 3.75V16.25" stroke="currentColor" stroke-linecap="square"/>`,
"arrow-up": `<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99991 2.24121L16.0921 8.33343L15.2083 9.21731L10.6249 4.63397V17.5001H9.37492V4.63398L4.7916 9.21731L3.90771 8.33343L9.99991 2.24121Z" fill="currentColor"/>`,
"arrow-left": `<path d="M8.33464 4.58398L2.91797 10.0007L8.33464 15.4173M3.33464 10.0007H17.0846" stroke="currentColor" stroke-linecap="square"/>`,
"arrow-right": `<path d="M11.6654 4.58398L17.082 10.0007L11.6654 15.4173M16.6654 10.0007H2.91536" stroke="currentColor" stroke-linecap="square"/>`,
archive: `<path d="M16.8747 6.24935H17.3747V5.74935H16.8747V6.24935ZM16.8747 16.8743V17.3743H17.3747V16.8743H16.8747ZM3.12467 16.8743H2.62467V17.3743H3.12467V16.8743ZM3.12467 6.24935V5.74935H2.62467V6.24935H3.12467ZM2.08301 2.91602V2.41602H1.58301V2.91602H2.08301ZM17.9163 2.91602H18.4163V2.41602H17.9163V2.91602ZM17.9163 6.24935V6.74935H18.4163V6.24935H17.9163ZM2.08301 6.24935H1.58301V6.74935H2.08301V6.24935ZM8.33301 9.08268H7.83301V10.0827H8.33301V9.58268V9.08268ZM11.6663 10.0827H12.1663V9.08268H11.6663V9.58268V10.0827ZM16.8747 6.24935H16.3747V16.8743H16.8747H17.3747V6.24935H16.8747ZM16.8747 16.8743V16.3743H3.12467V16.8743V17.3743H16.8747V16.8743ZM3.12467 16.8743H3.62467V6.24935H3.12467H2.62467V16.8743H3.12467ZM3.12467 6.24935V6.74935H16.8747V6.24935V5.74935H3.12467V6.24935ZM2.08301 2.91602V3.41602H17.9163V2.91602V2.41602H2.08301V2.91602ZM17.9163 2.91602H17.4163V6.24935H17.9163H18.4163V2.91602H17.9163ZM17.9163 6.24935V5.74935H2.08301V6.24935V6.74935H17.9163V6.24935ZM2.08301 6.24935H2.58301V2.91602H2.08301H1.58301V6.24935H2.08301ZM8.33301 9.58268V10.0827H11.6663V9.58268V9.08268H8.33301V9.58268Z" fill="currentColor"/>`,
"bubble-5": `<path d="M18.3327 9.99935C18.3327 5.57227 15.0919 2.91602 9.99935 2.91602C4.90676 2.91602 1.66602 5.57227 1.66602 9.99935C1.66602 11.1487 2.45505 13.1006 2.57637 13.3939C2.58707 13.4197 2.59766 13.4434 2.60729 13.4697C2.69121 13.6987 3.04209 14.9354 1.66602 16.7674C3.51787 17.6528 5.48453 16.1973 5.48453 16.1973C6.84518 16.9193 8.46417 17.0827 9.99935 17.0827C15.0919 17.0827 18.3327 14.4264 18.3327 9.99935Z" stroke="currentColor" stroke-linecap="square"/>`,
brain: `<path d="M13.332 8.7487C11.4911 8.7487 9.9987 7.25631 9.9987 5.41536M6.66536 11.2487C8.50631 11.2487 9.9987 12.7411 9.9987 14.582M9.9987 2.78209L9.9987 17.0658M16.004 15.0475C17.1255 14.5876 17.9154 13.4849 17.9154 12.1978C17.9154 11.3363 17.5615 10.5575 16.9913 9.9987C17.5615 9.43991 17.9154 8.66108 17.9154 7.79962C17.9154 6.21199 16.7136 4.90504 15.1702 4.73878C14.7858 3.21216 13.4039 2.08203 11.758 2.08203C11.1171 2.08203 10.5162 2.25337 9.9987 2.55275C9.48117 2.25337 8.88032 2.08203 8.23944 2.08203C6.59353 2.08203 5.21157 3.21216 4.82722 4.73878C3.28377 4.90504 2.08203 6.21199 2.08203 7.79962C2.08203 8.66108 2.43585 9.43991 3.00609 9.9987C2.43585 10.5575 2.08203 11.3363 2.08203 12.1978C2.08203 13.4849 2.87191 14.5876 3.99339 15.0475C4.46688 16.7033 5.9917 17.9154 7.79962 17.9154C8.61335 17.9154 9.36972 17.6698 9.9987 17.2488C10.6277 17.6698 11.384 17.9154 12.1978 17.9154C14.0057 17.9154 15.5305 16.7033 16.004 15.0475Z" stroke="currentColor"/>`,