app: prefer using useLocation instead of window.location (#15989)

pull/16104/head
Brendan Allan 2026-03-05 14:41:12 +08:00 committed by GitHub
parent f363904feb
commit 7948de1612
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 49 additions and 43 deletions

View File

@ -484,6 +484,7 @@
"@solid-primitives/media": "2.3.3", "@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/resize-observer": "2.1.3",
"@solidjs/meta": "catalog:", "@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"dompurify": "3.3.1", "dompurify": "3.3.1",
"fuzzysort": "catalog:", "fuzzysort": "catalog:",
"katex": "0.16.27", "katex": "0.16.27",

View File

@ -1,10 +1,4 @@
import { A, useNavigate, useParams } from "@solidjs/router" import type { Message, Session, TextPart, UserMessage } from "@opencode-ai/sdk/v2/client"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { useLayout, type LocalProject, getAvatarColors } from "@/context/layout"
import { useNotification } from "@/context/notification"
import { usePermission } from "@/context/permission"
import { base64Encode } from "@opencode-ai/util/encode"
import { Avatar } from "@opencode-ai/ui/avatar" import { Avatar } from "@opencode-ai/ui/avatar"
import { HoverCard } from "@opencode-ai/ui/hover-card" import { HoverCard } from "@opencode-ai/ui/hover-card"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
@ -12,12 +6,18 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
import { MessageNav } from "@opencode-ai/ui/message-nav" import { MessageNav } from "@opencode-ai/ui/message-nav"
import { Spinner } from "@opencode-ai/ui/spinner" import { Spinner } from "@opencode-ai/ui/spinner"
import { Tooltip } from "@opencode-ai/ui/tooltip" import { Tooltip } from "@opencode-ai/ui/tooltip"
import { base64Encode } from "@opencode-ai/util/encode"
import { getFilename } from "@opencode-ai/util/path" import { getFilename } from "@opencode-ai/util/path"
import { type Message, type Session, type TextPart, type UserMessage } from "@opencode-ai/sdk/v2/client" import { A, useNavigate, useParams } from "@solidjs/router"
import { For, Match, Show, Switch, createMemo, onCleanup, type Accessor, type JSX } from "solid-js" import { type Accessor, createMemo, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
import { useNotification } from "@/context/notification"
import { usePermission } from "@/context/permission"
import { agentColor } from "@/utils/agent" import { agentColor } from "@/utils/agent"
import { hasProjectPermissions } from "./helpers"
import { sessionPermissionRequest } from "../session/composer/session-request-tree" import { sessionPermissionRequest } from "../session/composer/session-request-tree"
import { hasProjectPermissions } from "./helpers"
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
@ -231,7 +231,9 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed()) const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
const isActive = createMemo(() => props.session.id === params.id) const isActive = createMemo(() => props.session.id === params.id)
const hoverPrefetch = { current: undefined as ReturnType<typeof setTimeout> | undefined } const hoverPrefetch = {
current: undefined as ReturnType<typeof setTimeout> | undefined,
}
const cancelHoverPrefetch = () => { const cancelHoverPrefetch = () => {
if (hoverPrefetch.current === undefined) return if (hoverPrefetch.current === undefined) return
clearTimeout(hoverPrefetch.current) clearTimeout(hoverPrefetch.current)
@ -300,17 +302,15 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
setHoverSession={props.setHoverSession} setHoverSession={props.setHoverSession}
messageLabel={messageLabel} messageLabel={messageLabel}
onMessageSelect={(message) => { onMessageSelect={(message) => {
if (!isActive()) { if (!isActive())
layout.pendingMessage.set(`${base64Encode(props.session.directory)}/${props.session.id}`, message.id) layout.pendingMessage.set(`${base64Encode(props.session.directory)}/${props.session.id}`, message.id)
navigate(`${props.slug}/session/${props.session.id}`)
return navigate(`${props.slug}/session/${props.session.id}#message-${message.id}`)
}
window.history.replaceState(null, "", `#message-${message.id}`)
window.dispatchEvent(new HashChangeEvent("hashchange"))
}} }}
trigger={item} trigger={item}
/> />
</Show> </Show>
<div <div
class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`} class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`}
classList={{ classList={{

View File

@ -0,0 +1,6 @@
export const messageIdFromHash = (hash: string) => {
const value = hash.startsWith("#") ? hash.slice(1) : hash
const match = value.match(/^message-(.+)$/)
if (!match) return
return match[1]
}

View File

@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { messageIdFromHash } from "./use-session-hash-scroll" import { messageIdFromHash } from "./message-id-from-hash"
describe("messageIdFromHash", () => { describe("messageIdFromHash", () => {
test("parses hash with leading #", () => { test("parses hash with leading #", () => {

View File

@ -1,12 +1,9 @@
import { createEffect, createMemo, onCleanup, onMount } from "solid-js" import type { UserMessage } from "@opencode-ai/sdk/v2"
import { UserMessage } from "@opencode-ai/sdk/v2" import { useLocation, useNavigate } from "@solidjs/router"
import { createEffect, createMemo, onMount } from "solid-js"
import { messageIdFromHash } from "./message-id-from-hash"
export const messageIdFromHash = (hash: string) => { export { messageIdFromHash } from "./message-id-from-hash"
const value = hash.startsWith("#") ? hash.slice(1) : hash
const match = value.match(/^message-(.+)$/)
if (!match) return
return match[1]
}
export const useSessionHashScroll = (input: { export const useSessionHashScroll = (input: {
sessionKey: () => string sessionKey: () => string
@ -30,13 +27,18 @@ export const useSessionHashScroll = (input: {
const messageIndex = createMemo(() => new Map(visibleUserMessages().map((m, i) => [m.id, i]))) const messageIndex = createMemo(() => new Map(visibleUserMessages().map((m, i) => [m.id, i])))
let pendingKey = "" let pendingKey = ""
const location = useLocation()
const navigate = useNavigate()
const clearMessageHash = () => { const clearMessageHash = () => {
if (!window.location.hash) return if (!location.hash) return
window.history.replaceState(null, "", window.location.href.replace(/#.*$/, "")) navigate(location.pathname + location.search, { replace: true })
} }
const updateHash = (id: string) => { const updateHash = (id: string) => {
window.history.replaceState(null, "", `#${input.anchor(id)}`) navigate(location.pathname + location.search + `#${input.anchor(id)}`, {
replace: true,
})
} }
const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => { const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => {
@ -53,6 +55,7 @@ export const useSessionHashScroll = (input: {
} }
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => { const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
console.log({ message, behavior })
if (input.currentMessageId() !== message.id) input.setActiveMessage(message) if (input.currentMessageId() !== message.id) input.setActiveMessage(message)
const index = messageIndex().get(message.id) ?? -1 const index = messageIndex().get(message.id) ?? -1
@ -100,7 +103,7 @@ export const useSessionHashScroll = (input: {
} }
const applyHash = (behavior: ScrollBehavior) => { const applyHash = (behavior: ScrollBehavior) => {
const hash = window.location.hash.slice(1) const hash = location.hash.slice(1)
if (!hash) { if (!hash) {
input.autoScroll.forceScrollToBottom() input.autoScroll.forceScrollToBottom()
const el = input.scroller() const el = input.scroller()
@ -132,6 +135,7 @@ export const useSessionHashScroll = (input: {
} }
createEffect(() => { createEffect(() => {
location.hash
if (!input.sessionID() || !input.messagesReady()) return if (!input.sessionID() || !input.messagesReady()) return
requestAnimationFrame(() => applyHash("auto")) requestAnimationFrame(() => applyHash("auto"))
}) })
@ -155,7 +159,7 @@ export const useSessionHashScroll = (input: {
} }
} }
if (!targetId) targetId = messageIdFromHash(window.location.hash) if (!targetId) targetId = messageIdFromHash(location.hash)
if (!targetId) return if (!targetId) return
if (input.currentMessageId() === targetId) return if (input.currentMessageId() === targetId) return
@ -171,14 +175,6 @@ export const useSessionHashScroll = (input: {
if (typeof window !== "undefined" && "scrollRestoration" in window.history) { if (typeof window !== "undefined" && "scrollRestoration" in window.history) {
window.history.scrollRestoration = "manual" window.history.scrollRestoration = "manual"
} }
const handler = () => {
if (!input.sessionID() || !input.messagesReady()) return
requestAnimationFrame(() => applyHash("auto"))
}
window.addEventListener("hashchange", handler)
onCleanup(() => window.removeEventListener("hashchange", handler))
}) })
return { return {

View File

@ -7,6 +7,7 @@ export const setNavigate = (fn: (href: string) => void) => {
export const handleNotificationClick = (href?: string) => { export const handleNotificationClick = (href?: string) => {
window.focus() window.focus()
if (!href) return if (!href) return
if (nav) nav(href) if (nav) return nav(href)
else window.location.assign(href) console.warn("notification-click: navigate function not set, falling back to window.location.assign")
window.location.assign(href)
} }

View File

@ -51,6 +51,7 @@
"@solid-primitives/media": "2.3.3", "@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/resize-observer": "2.1.3",
"@solidjs/meta": "catalog:", "@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"dompurify": "3.3.1", "dompurify": "3.3.1",
"fuzzysort": "catalog:", "fuzzysort": "catalog:",
"katex": "0.16.27", "katex": "0.16.27",

View File

@ -52,6 +52,7 @@ import { TextShimmer } from "./text-shimmer"
import { AnimatedCountList } from "./tool-count-summary" import { AnimatedCountList } from "./tool-count-summary"
import { ToolStatusTitle } from "./tool-status-title" import { ToolStatusTitle } from "./tool-status-title"
import { animate } from "motion" import { animate } from "motion"
import { useLocation } from "@solidjs/router"
function ShellSubmessage(props: { text: string; animate?: boolean }) { function ShellSubmessage(props: { text: string; animate?: boolean }) {
let widthRef: HTMLSpanElement | undefined let widthRef: HTMLSpanElement | undefined
@ -1471,6 +1472,7 @@ ToolRegistry.register({
render(props) { render(props) {
const data = useData() const data = useData()
const i18n = useI18n() const i18n = useI18n()
const location = useLocation()
const childSessionId = () => props.metadata.sessionId as string | undefined const childSessionId = () => props.metadata.sessionId as string | undefined
const title = createMemo(() => i18n.t("ui.tool.agent", { type: props.input.subagent_type || props.tool })) const title = createMemo(() => i18n.t("ui.tool.agent", { type: props.input.subagent_type || props.tool }))
const description = createMemo(() => { const description = createMemo(() => {
@ -1487,8 +1489,7 @@ ToolRegistry.register({
const direct = data.sessionHref?.(sessionId) const direct = data.sessionHref?.(sessionId)
if (direct) return direct if (direct) return direct
if (typeof window === "undefined") return const path = location.pathname
const path = window.location.pathname
const idx = path.indexOf("/session") const idx = path.indexOf("/session")
if (idx === -1) return if (idx === -1) return
return `${path.slice(0, idx)}/session/${sessionId}` return `${path.slice(0, idx)}/session/${sessionId}`