Procházet zdrojové kódy

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

Brendan Allan před 1 měsícem
rodič
revize
7948de1612

+ 1 - 0
bun.lock

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

+ 17 - 17
packages/app/src/pages/layout/sidebar-items.tsx

@@ -1,10 +1,4 @@
-import { A, useNavigate, useParams } from "@solidjs/router"
-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 type { Message, Session, TextPart, UserMessage } from "@opencode-ai/sdk/v2/client"
 import { Avatar } from "@opencode-ai/ui/avatar"
 import { HoverCard } from "@opencode-ai/ui/hover-card"
 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 { Spinner } from "@opencode-ai/ui/spinner"
 import { Tooltip } from "@opencode-ai/ui/tooltip"
+import { base64Encode } from "@opencode-ai/util/encode"
 import { getFilename } from "@opencode-ai/util/path"
-import { type Message, type Session, type TextPart, type UserMessage } from "@opencode-ai/sdk/v2/client"
-import { For, Match, Show, Switch, createMemo, onCleanup, type Accessor, type JSX } from "solid-js"
+import { A, useNavigate, useParams } from "@solidjs/router"
+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 { hasProjectPermissions } from "./helpers"
 import { sessionPermissionRequest } from "../session/composer/session-request-tree"
+import { hasProjectPermissions } from "./helpers"
 
 const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
 
@@ -231,7 +231,9 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
   const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
   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 = () => {
     if (hoverPrefetch.current === undefined) return
     clearTimeout(hoverPrefetch.current)
@@ -300,17 +302,15 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
           setHoverSession={props.setHoverSession}
           messageLabel={messageLabel}
           onMessageSelect={(message) => {
-            if (!isActive()) {
+            if (!isActive())
               layout.pendingMessage.set(`${base64Encode(props.session.directory)}/${props.session.id}`, message.id)
-              navigate(`${props.slug}/session/${props.session.id}`)
-              return
-            }
-            window.history.replaceState(null, "", `#message-${message.id}`)
-            window.dispatchEvent(new HashChangeEvent("hashchange"))
+
+            navigate(`${props.slug}/session/${props.session.id}#message-${message.id}`)
           }}
           trigger={item}
         />
       </Show>
+
       <div
         class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`}
         classList={{

+ 6 - 0
packages/app/src/pages/session/message-id-from-hash.ts

@@ -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]
+}

+ 1 - 1
packages/app/src/pages/session/use-session-hash-scroll.test.ts

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

+ 18 - 22
packages/app/src/pages/session/use-session-hash-scroll.ts

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

+ 3 - 2
packages/app/src/utils/notification-click.ts

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

+ 1 - 0
packages/ui/package.json

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

+ 3 - 2
packages/ui/src/components/message-part.tsx

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