From 9128e847bdbf9e5a58b7bdc33896e484e745aedd Mon Sep 17 00:00:00 2001 From: David Hill <1879069+iamdavidhill@users.noreply.github.com> Date: Wed, 9 Sep 2026 01:17:11 -0600 Subject: [PATCH] fix(app): refine browser toolbar and empty state (#48061) Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com> --- packages/app/src/runtime/i18n/en.ts | 3 +- packages/app/src/session/browser/pane.tsx | 113 +++++++++++++----- .../src/session/files/session-side-panel.tsx | 10 +- packages/app/src/shell/commands/command.tsx | 2 +- packages/desktop/src/main/browser-chromium.ts | 8 +- packages/ui/src/icons/icon/icon.tsx | 8 ++ 6 files changed, 109 insertions(+), 35 deletions(-) diff --git a/packages/app/src/runtime/i18n/en.ts b/packages/app/src/runtime/i18n/en.ts index 0a962115379..12966f6b37c 100644 --- a/packages/app/src/runtime/i18n/en.ts +++ b/packages/app/src/runtime/i18n/en.ts @@ -107,6 +107,7 @@ export const dict = { "command.session.new": "New session", "command.file.open": "Open file", "command.browser.open": "Open browser", + "command.browser.reload": "Reload browser page", "command.tab.close": "Close tab", "command.tab.reopenClosed": "Reopen closed tab", "command.context.addSelection": "Add selection to context", @@ -897,7 +898,7 @@ export const dict = { "session.browser.address": "Browser address", "session.browser.replaced": "Browser control moved to another desktop window.", "session.browser.suspended": "Browser suspended. Interact with this session to reconnect.", - "session.browser.address.placeholder": "Enter a URL", + "session.browser.address.placeholder": "Enter URL", "titlebar.update": "Update", "titlebar.tabs": "Tabs", diff --git a/packages/app/src/session/browser/pane.tsx b/packages/app/src/session/browser/pane.tsx index ed895143f67..fbfdf03a44d 100644 --- a/packages/app/src/session/browser/pane.tsx +++ b/packages/app/src/session/browser/pane.tsx @@ -1,6 +1,8 @@ import { Icon } from "@opencode/ui/icon" import { IconButton } from "@opencode/ui/icon-button" import { Loader } from "@opencode/ui/loader" +import { Keybind } from "@opencode/ui/keybind" +import { Tooltip } from "@opencode/ui/tooltip" import { useDialog } from "@opencode/ui/context/dialog" import { createEventListener } from "@solid-primitives/event-listener" import { createResizeObserver } from "@solid-primitives/resize-observer" @@ -8,13 +10,16 @@ import { createEffect, For, on, onCleanup, Show } from "solid-js" import { createStore } from "solid-js/store" import { useLanguage } from "@/runtime/i18n/language" import { usePlatform } from "@/runtime/platform/platform" +import { useCommand } from "@/shell/commands/command" import type { createSessionBrowser } from "./model" export function SessionBrowserPane(props: { browser: ReturnType; visible: boolean }) { const platform = usePlatform() const language = useLanguage() const dialog = useDialog() + const command = useCommand() const state = props.browser.active + const address = () => (state()?.url === "about:blank" ? "" : (state()?.url ?? "")) const registration = props.browser.registration const button = { variant: "ghost", size: "large" } as const const [store, setStore] = createStore({ @@ -23,12 +28,28 @@ export function SessionBrowserPane(props: { browser: ReturnType store.address.match(/^https?:\/\//i)?.[0] ?? "" + + command.register("browser.navigation", () => [ + { + id: "browser.reload", + title: language.t("command.browser.reload"), + category: language.t("command.category.view"), + keybind: "f5", + disabled: !props.visible || !state(), + onSelect: () => { + const tab = state() + if (tab) props.browser.command({ type: "reload", tabID: tab.id }) + }, + }, + ]) // The native page always paints above the DOM, so hide it while a floating // menu, select, or popover overlaps it. Tooltips are excluded. @@ -86,7 +107,7 @@ export function SessionBrowserPane(props: { browser: ReturnType !store.editing && setStore("address", state()?.url ?? "")) + createEffect(() => !store.editing && setStore("address", address())) createEffect( on( [ @@ -124,37 +145,58 @@ export function SessionBrowserPane(props: { browser: ReturnType -
+
{(direction) => ( - { - const tab = state() - if (tab) props.browser.command({ type: direction, tabID: tab.id }) - }} - icon={} - /> + + { + const tab = state() + if (tab) props.browser.command({ type: direction, tabID: tab.id }) + }} + icon={ + + } + /> + )} - { - const tab = state() - if (tab) props.browser.command({ type: tab.loading ? "stop" : "reload", tabID: tab.id }) - }} - icon={ - }> - - + + {language.t(state()?.loading ? "prompt.action.stop" : "error.page.action.reload")} + + + +
} - /> + > + { + const tab = state() + if (tab) props.browser.command({ type: tab.loading ? "stop" : "reload", tabID: tab.id }) + }} + icon={ + }> + + + } + /> +
{ event.preventDefault() const tab = state() @@ -163,15 +205,30 @@ export function SessionBrowserPane(props: { browser: ReturnType setStore("editing", true)} - onBlur={() => setStore({ editing: false, address: state()?.url ?? "" })} + onBlur={() => setStore({ editing: false, address: address() })} onInput={(event) => setStore("address", event.currentTarget.value)} + onScroll={(event) => { + if (addressDisplay) addressDisplay.scrollLeft = event.currentTarget.scrollLeft + }} /> + {/* Keep native input editing and selection while coloring the scheme, including during editing. */} +
diff --git a/packages/app/src/session/files/session-side-panel.tsx b/packages/app/src/session/files/session-side-panel.tsx index 06d5422315f..ee2723007ed 100644 --- a/packages/app/src/session/files/session-side-panel.tsx +++ b/packages/app/src/session/files/session-side-panel.tsx @@ -396,9 +396,11 @@ export function SessionSidePanel(props: { ariaControls={activeTab() === tab ? browserTabPanelID : undefined} >
- + - {item().title || language.t("session.tab.browser")} + {!item().url || item().url === "about:blank" + ? language.t("session.tab.browser") + : item().title || item().url}
@@ -508,7 +510,7 @@ export function SessionSidePanel(props: { } >
- + {language.t("command.file.open")}
@@ -522,7 +524,7 @@ export function SessionSidePanel(props: { } >
- + {language.t("session.tab.browser")}
diff --git a/packages/app/src/shell/commands/command.tsx b/packages/app/src/shell/commands/command.tsx index 494c0e77582..0a469539ddb 100644 --- a/packages/app/src/shell/commands/command.tsx +++ b/packages/app/src/shell/commands/command.tsx @@ -15,7 +15,7 @@ const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(na const PALETTE_ID = "command.palette" export const DEFAULT_PALETTE_KEYBIND = "mod+k,mod+shift+p" const SUGGESTED_PREFIX = "suggested." -const EDITABLE_KEYBIND_IDS = new Set(["terminal.toggle", "terminal.new", "file.attach"]) +const EDITABLE_KEYBIND_IDS = new Set(["terminal.toggle", "terminal.new", "file.attach", "browser.reload"]) type KeyLabel = | "common.key.ctrl" diff --git a/packages/desktop/src/main/browser-chromium.ts b/packages/desktop/src/main/browser-chromium.ts index e5eb649ae18..f01121728f1 100644 --- a/packages/desktop/src/main/browser-chromium.ts +++ b/packages/desktop/src/main/browser-chromium.ts @@ -58,7 +58,13 @@ export function createBrowserPage( const contents = view.webContents const detachNetwork = options.network?.attach(contents) contents.on("before-input-event", (event, input) => { - if (input.type !== "keyDown" || input.alt || !(process.platform === "darwin" ? input.meta : input.control)) return + if (input.type !== "keyDown") return + if (input.key === "F5" && !input.meta && !input.control && !input.alt && !input.shift) { + event.preventDefault() + contents.reload() + return + } + if (input.alt || !(process.platform === "darwin" ? input.meta : input.control)) return const step = input.key === "=" || input.key === "+" || input.code === "NumpadAdd" ? 0.5 diff --git a/packages/ui/src/icons/icon/icon.tsx b/packages/ui/src/icons/icon/icon.tsx index fdc49ca29ff..c8c4812c09d 100644 --- a/packages/ui/src/icons/icon/icon.tsx +++ b/packages/ui/src/icons/icon/icon.tsx @@ -8,6 +8,10 @@ const icons = { viewBox: "0 0 16 16", body: ``, }, + globe: { + viewBox: "0 0 16 16", + body: ``, + }, flask: { viewBox: "0 0 16 16", body: ``, @@ -196,6 +200,10 @@ const icons = { viewBox: "0 0 20 20", body: ``, }, + refresh: { + viewBox: "0 0 20 20", + body: ``, + }, reset: { viewBox: "0 0 20 20", body: ``,