summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/pages
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-03-02 10:50:50 -0600
committerGitHub <[email protected]>2026-03-02 10:50:50 -0600
commit8176bafc555e562ade48a675dffa3f38751ed8c9 (patch)
tree7d4b0f6e98f431999b89c1f24687f6f53bd0bc6b /packages/app/src/pages
parent0a3a3216db5974efd3edc9a213054fd97d8dbd34 (diff)
downloadopencode-8176bafc555e562ade48a675dffa3f38751ed8c9.tar.gz
opencode-8176bafc555e562ade48a675dffa3f38751ed8c9.zip
chore(app): solidjs refactoring (#13399)
Diffstat (limited to 'packages/app/src/pages')
-rw-r--r--packages/app/src/pages/layout.tsx307
-rw-r--r--packages/app/src/pages/layout/helpers.ts30
-rw-r--r--packages/app/src/pages/session.tsx18
-rw-r--r--packages/app/src/pages/session/file-tabs.tsx63
-rw-r--r--packages/app/src/pages/session/review-tab.tsx34
-rw-r--r--packages/app/src/pages/session/terminal-panel.tsx6
-rw-r--r--packages/app/src/pages/session/use-session-hash-scroll.ts36
7 files changed, 263 insertions, 231 deletions
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index cb194052d..f93d1f069 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -59,11 +59,11 @@ import { useLanguage, type Locale } from "@/context/language"
import {
childMapByParent,
displayName,
+ effectiveWorkspaceOrder,
errorMessage,
getDraggableId,
latestRootSession,
sortedRootSessions,
- syncWorkspaceOrder,
workspaceKey,
} from "./layout/helpers"
import { collectOpenProjectDeepLinks, deepLinkEvent, drainPendingDeepLinks } from "./layout/deep-links"
@@ -483,21 +483,6 @@ export default function Layout(props: ParentProps) {
createEffect(
on(
- () => ({ ready: pageReady(), project: currentProject() }),
- (value) => {
- if (!value.ready) return
- const project = value.project
- if (!project) return
- const last = server.projects.last()
- if (last === project.worktree) return
- server.projects.touch(project.worktree)
- },
- { defer: true },
- ),
- )
-
- createEffect(
- on(
() => ({ ready: pageReady(), layoutReady: layoutReady(), dir: params.dir, list: layout.projects.list() }),
(value) => {
if (!value.ready) return
@@ -554,29 +539,17 @@ export default function Layout(props: ParentProps) {
return layout.sidebar.workspaces(project.worktree)()
})
- createEffect(() => {
- if (!pageReady()) return
- if (!layoutReady()) return
+ const visibleSessionDirs = createMemo(() => {
const project = currentProject()
- if (!project) return
-
- const local = project.worktree
- const dirs = [project.worktree, ...(project.sandboxes ?? [])]
- const existing = store.workspaceOrder[project.worktree]
- const merged = syncWorkspaceOrder(local, dirs, existing)
- if (!existing) {
- setStore("workspaceOrder", project.worktree, merged)
- return
- }
-
- if (merged.length !== existing.length) {
- setStore("workspaceOrder", project.worktree, merged)
- return
- }
-
- if (merged.some((d, i) => d !== existing[i])) {
- setStore("workspaceOrder", project.worktree, merged)
- }
+ if (!project) return [] as string[]
+ if (!workspaceSetting()) return [project.worktree]
+
+ const activeDir = currentDir()
+ return workspaceIds(project).filter((directory) => {
+ const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
+ const active = directory === activeDir
+ return expanded || active
+ })
})
createEffect(() => {
@@ -593,25 +566,17 @@ export default function Layout(props: ParentProps) {
})
const currentSessions = createMemo(() => {
- const project = currentProject()
- if (!project) return [] as Session[]
const now = Date.now()
- if (workspaceSetting()) {
- const dirs = workspaceIds(project)
- const activeDir = currentDir()
- const result: Session[] = []
- for (const dir of dirs) {
- const expanded = store.workspaceExpanded[dir] ?? dir === project.worktree
- const active = dir === activeDir
- if (!expanded && !active) continue
- const [dirStore] = globalSync.child(dir, { bootstrap: true })
- const dirSessions = sortedRootSessions(dirStore, now)
- result.push(...dirSessions)
- }
- return result
+ const dirs = visibleSessionDirs()
+ if (dirs.length === 0) return [] as Session[]
+
+ const result: Session[] = []
+ for (const dir of dirs) {
+ const [dirStore] = globalSync.child(dir, { bootstrap: true })
+ const dirSessions = sortedRootSessions(dirStore, now)
+ result.push(...dirSessions)
}
- const [projectStore] = globalSync.child(project.worktree)
- return sortedRootSessions(projectStore, now)
+ return result
})
type PrefetchQueue = {
@@ -826,7 +791,6 @@ export default function Layout(props: ParentProps) {
}
navigateToSession(session)
- queueMicrotask(() => scrollToSession(session.id, `${session.directory}:${session.id}`))
}
function navigateSessionByUnseen(offset: number) {
@@ -861,7 +825,6 @@ export default function Layout(props: ParentProps) {
}
navigateToSession(session)
- queueMicrotask(() => scrollToSession(session.id, `${session.directory}:${session.id}`))
return
}
}
@@ -1094,34 +1057,90 @@ export default function Layout(props: ParentProps) {
return meta?.worktree ?? directory
}
+ function activeProjectRoot(directory: string) {
+ return currentProject()?.worktree ?? projectRoot(directory)
+ }
+
+ function touchProjectRoute() {
+ const root = currentProject()?.worktree
+ if (!root) return
+ if (server.projects.last() !== root) server.projects.touch(root)
+ return root
+ }
+
+ function rememberSessionRoute(directory: string, id: string, root = activeProjectRoot(directory)) {
+ setStore("lastProjectSession", root, { directory, id, at: Date.now() })
+ return root
+ }
+
+ function clearLastProjectSession(root: string) {
+ if (!store.lastProjectSession[root]) return
+ setStore(
+ "lastProjectSession",
+ produce((draft) => {
+ delete draft[root]
+ }),
+ )
+ }
+
+ function syncSessionRoute(directory: string, id: string, root = activeProjectRoot(directory)) {
+ rememberSessionRoute(directory, id, root)
+ notification.session.markViewed(id)
+ const expanded = untrack(() => store.workspaceExpanded[directory])
+ if (expanded === false) {
+ setStore("workspaceExpanded", directory, true)
+ }
+ requestAnimationFrame(() => scrollToSession(id, `${directory}:${id}`))
+ return root
+ }
+
async function navigateToProject(directory: string | undefined) {
if (!directory) return
const root = projectRoot(directory)
server.projects.touch(root)
const project = layout.projects.list().find((item) => item.worktree === root)
- const dirs = Array.from(new Set([root, ...(store.workspaceOrder[root] ?? []), ...(project?.sandboxes ?? [])]))
+ let dirs = project
+ ? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root])
+ : [root]
+ const canOpen = (value: string | undefined) => {
+ if (!value) return false
+ return dirs.some((item) => workspaceKey(item) === workspaceKey(value))
+ }
+ const refreshDirs = async (target?: string) => {
+ if (!target || target === root || canOpen(target)) return canOpen(target)
+ const listed = await globalSDK.client.worktree
+ .list({ directory: root })
+ .then((x) => x.data ?? [])
+ .catch(() => [] as string[])
+ dirs = effectiveWorkspaceOrder(root, [root, ...listed], store.workspaceOrder[root])
+ return canOpen(target)
+ }
const openSession = async (target: { directory: string; id: string }) => {
+ if (!canOpen(target.directory)) return false
const resolved = await globalSDK.client.session
.get({ sessionID: target.id })
.then((x) => x.data)
.catch(() => undefined)
- const next = resolved?.directory ? resolved : target
- setStore("lastProjectSession", root, { directory: next.directory, id: next.id, at: Date.now() })
- navigateWithSidebarReset(`/${base64Encode(next.directory)}/session/${next.id}`)
+ if (!resolved?.directory) return false
+ if (!canOpen(resolved.directory)) return false
+ setStore("lastProjectSession", root, { directory: resolved.directory, id: resolved.id, at: Date.now() })
+ navigateWithSidebarReset(`/${base64Encode(resolved.directory)}/session/${resolved.id}`)
+ return true
}
const projectSession = store.lastProjectSession[root]
if (projectSession?.id) {
- await openSession(projectSession)
- return
+ await refreshDirs(projectSession.directory)
+ const opened = await openSession(projectSession)
+ if (opened) return
+ clearLastProjectSession(root)
}
const latest = latestRootSession(
dirs.map((item) => globalSync.child(item, { bootstrap: false })[0]),
Date.now(),
)
- if (latest) {
- await openSession(latest)
+ if (latest && (await openSession(latest))) {
return
}
@@ -1137,8 +1156,7 @@ export default function Layout(props: ParentProps) {
),
Date.now(),
)
- if (fetched) {
- await openSession(fetched)
+ if (fetched && (await openSession(fetched))) {
return
}
@@ -1240,9 +1258,17 @@ export default function Layout(props: ParentProps) {
}
}
- const deleteWorkspace = async (root: string, directory: string) => {
+ const deleteWorkspace = async (root: string, directory: string, leaveDeletedWorkspace = false) => {
if (directory === root) return
+ const current = currentDir()
+ const currentKey = workspaceKey(current)
+ const deletedKey = workspaceKey(directory)
+ const shouldLeave = leaveDeletedWorkspace || (!!params.dir && currentKey === deletedKey)
+ if (!leaveDeletedWorkspace && shouldLeave) {
+ navigateWithSidebarReset(`/${base64Encode(root)}/session`)
+ }
+
setBusy(directory, true)
const result = await globalSDK.client.worktree
@@ -1260,6 +1286,10 @@ export default function Layout(props: ParentProps) {
if (!result) return
+ if (workspaceKey(store.lastProjectSession[root]?.directory ?? "") === workspaceKey(directory)) {
+ clearLastProjectSession(root)
+ }
+
globalSync.set(
"project",
produce((draft) => {
@@ -1273,8 +1303,18 @@ export default function Layout(props: ParentProps) {
layout.projects.close(directory)
layout.projects.open(root)
- if (params.dir && currentDir() === directory) {
- navigateToProject(root)
+ if (shouldLeave) return
+
+ const nextCurrent = currentDir()
+ const nextKey = workspaceKey(nextCurrent)
+ const project = layout.projects.list().find((item) => item.worktree === root)
+ const dirs = project
+ ? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root])
+ : [root]
+ const valid = dirs.some((item) => workspaceKey(item) === nextKey)
+
+ if (params.dir && projectRoot(nextCurrent) === root && !valid) {
+ navigateWithSidebarReset(`/${base64Encode(root)}/session`)
}
}
@@ -1377,8 +1417,12 @@ export default function Layout(props: ParentProps) {
})
const handleDelete = () => {
+ const leaveDeletedWorkspace = !!params.dir && workspaceKey(currentDir()) === workspaceKey(props.directory)
+ if (leaveDeletedWorkspace) {
+ navigateWithSidebarReset(`/${base64Encode(props.root)}/session`)
+ }
dialog.close()
- void deleteWorkspace(props.root, props.directory)
+ void deleteWorkspace(props.root, props.directory, leaveDeletedWorkspace)
}
const description = () => {
@@ -1486,26 +1530,42 @@ export default function Layout(props: ParentProps) {
)
}
+ const activeRoute = {
+ session: "",
+ sessionProject: "",
+ }
+
createEffect(
on(
- () => ({ ready: pageReady(), dir: params.dir, id: params.id }),
- (value) => {
- if (!value.ready) return
- const dir = value.dir
- const id = value.id
- if (!dir || !id) return
+ () => [pageReady(), params.dir, params.id, currentProject()?.worktree] as const,
+ ([ready, dir, id]) => {
+ if (!ready || !dir) {
+ activeRoute.session = ""
+ activeRoute.sessionProject = ""
+ return
+ }
+
const directory = decode64(dir)
if (!directory) return
- const at = Date.now()
- setStore("lastProjectSession", projectRoot(directory), { directory, id, at })
- notification.session.markViewed(id)
- const expanded = untrack(() => store.workspaceExpanded[directory])
- if (expanded === false) {
- setStore("workspaceExpanded", directory, true)
+
+ const root = touchProjectRoute() ?? activeProjectRoot(directory)
+
+ if (!id) {
+ activeRoute.session = ""
+ activeRoute.sessionProject = ""
+ return
+ }
+
+ const session = `${dir}/${id}`
+ if (session !== activeRoute.session) {
+ activeRoute.session = session
+ activeRoute.sessionProject = syncSessionRoute(directory, id, root)
+ return
}
- requestAnimationFrame(() => scrollToSession(id, `${directory}:${id}`))
+
+ if (root === activeRoute.sessionProject) return
+ activeRoute.sessionProject = rememberSessionRoute(directory, id, root)
},
- { defer: true },
),
)
@@ -1516,40 +1576,29 @@ export default function Layout(props: ParentProps) {
const loadedSessionDirs = new Set<string>()
- createEffect(() => {
- const project = currentProject()
- const workspaces = workspaceSetting()
- const next = new Set<string>()
- if (!project) {
- loadedSessionDirs.clear()
- return
- }
-
- if (workspaces) {
- const activeDir = currentDir()
- const dirs = [project.worktree, ...(project.sandboxes ?? [])]
- for (const directory of dirs) {
- const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
- const active = directory === activeDir
- if (!expanded && !active) continue
- next.add(directory)
- }
- }
-
- if (!workspaces) {
- next.add(project.worktree)
- }
+ createEffect(
+ on(
+ visibleSessionDirs,
+ (dirs) => {
+ if (dirs.length === 0) {
+ loadedSessionDirs.clear()
+ return
+ }
- for (const directory of next) {
- if (loadedSessionDirs.has(directory)) continue
- globalSync.project.loadSessions(directory)
- }
+ const next = new Set(dirs)
+ for (const directory of next) {
+ if (loadedSessionDirs.has(directory)) continue
+ globalSync.project.loadSessions(directory)
+ }
- loadedSessionDirs.clear()
- for (const directory of next) {
- loadedSessionDirs.add(directory)
- }
- })
+ loadedSessionDirs.clear()
+ for (const directory of next) {
+ loadedSessionDirs.add(directory)
+ }
+ },
+ { defer: true },
+ ),
+ )
function handleDragStart(event: unknown) {
const id = getDraggableId(event)
@@ -1583,14 +1632,11 @@ export default function Layout(props: ParentProps) {
const extra = directory && directory !== local && !dirs.includes(directory) ? directory : undefined
const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false
- const existing = store.workspaceOrder[project.worktree]
- if (!existing) return extra ? [...dirs, extra] : dirs
-
- const merged = syncWorkspaceOrder(local, dirs, existing)
- if (pending && extra) return [local, extra, ...merged.filter((directory) => directory !== local)]
- if (!extra) return merged
- if (pending) return merged
- return [...merged, extra]
+ const ordered = effectiveWorkspaceOrder(local, dirs, store.workspaceOrder[project.worktree])
+ if (pending && extra) return [local, extra, ...ordered.filter((item) => item !== local)]
+ if (!extra) return ordered
+ if (pending) return ordered
+ return [...ordered, extra]
}
const sidebarProject = createMemo(() => {
@@ -1623,7 +1669,11 @@ export default function Layout(props: ParentProps) {
const [item] = result.splice(fromIndex, 1)
if (!item) return
result.splice(toIndex, 0, item)
- setStore("workspaceOrder", project.worktree, result)
+ setStore(
+ "workspaceOrder",
+ project.worktree,
+ result.filter((directory) => workspaceKey(directory) !== workspaceKey(project.worktree)),
+ )
}
function handleWorkspaceDragEnd() {
@@ -1661,10 +1711,9 @@ export default function Layout(props: ParentProps) {
const existing = prev ?? []
const next = existing.filter((item) => {
const id = workspaceKey(item)
- if (id === root) return false
- return id !== key
+ return id !== root && id !== key
})
- return [local, created.directory, ...next]
+ return [created.directory, ...next]
})
globalSync.child(created.directory)
diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts
index 2c4b834be..42315e589 100644
--- a/packages/app/src/pages/layout/helpers.ts
+++ b/packages/app/src/pages/layout/helpers.ts
@@ -74,9 +74,29 @@ export const errorMessage = (err: unknown, fallback: string) => {
return fallback
}
-export const syncWorkspaceOrder = (local: string, dirs: string[], existing?: string[]) => {
- if (!existing) return dirs
- const keep = existing.filter((d) => d !== local && dirs.includes(d))
- const missing = dirs.filter((d) => d !== local && !existing.includes(d))
- return [local, ...missing, ...keep]
+export const effectiveWorkspaceOrder = (local: string, dirs: string[], persisted?: string[]) => {
+ const root = workspaceKey(local)
+ const live = new Map<string, string>()
+
+ for (const dir of dirs) {
+ const key = workspaceKey(dir)
+ if (key === root) continue
+ if (!live.has(key)) live.set(key, dir)
+ }
+
+ if (!persisted?.length) return [local, ...live.values()]
+
+ const result = [local]
+ for (const dir of persisted) {
+ const key = workspaceKey(dir)
+ if (key === root) continue
+ const match = live.get(key)
+ if (!match) continue
+ result.push(match)
+ live.delete(key)
+ }
+
+ return [...result, ...live.values()]
}
+
+export const syncWorkspaceOrder = effectiveWorkspaceOrder
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 16b38cbd3..4f01badf4 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -347,24 +347,6 @@ export default function Page() {
if (path) file.load(path)
})
- createEffect(() => {
- const current = tabs().all()
- if (current.length === 0) return
-
- const next = normalizeTabs(current)
- if (same(current, next)) return
-
- tabs().setAll(next)
-
- const active = tabs().active()
- if (!active) return
- if (!active.startsWith("file://")) return
-
- const normalized = normalizeTab(active)
- if (active === normalized) return
- tabs().setActive(normalized)
- })
-
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx
index e92eee670..93264353b 100644
--- a/packages/app/src/pages/session/file-tabs.tsx
+++ b/packages/app/src/pages/session/file-tabs.tsx
@@ -67,6 +67,7 @@ export function FileTabContent(props: { tab: string }) {
let scroll: HTMLDivElement | undefined
let scrollFrame: number | undefined
+ let restoreFrame: number | undefined
let pending: { x: number; y: number } | undefined
let codeScroll: HTMLElement[] = []
let find: FileSearchHandle | null = null
@@ -349,6 +350,15 @@ export function FileTabContent(props: { tab: string }) {
if (el.scrollLeft !== s.x) el.scrollLeft = s.x
}
+ const queueRestore = () => {
+ if (restoreFrame !== undefined) return
+
+ restoreFrame = requestAnimationFrame(() => {
+ restoreFrame = undefined
+ restoreScroll()
+ })
+ }
+
const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
if (codeScroll.length === 0) syncCodeScroll()
@@ -364,46 +374,29 @@ export function FileTabContent(props: { tab: string }) {
setNote("commenting", null)
}
- createEffect(
- on(
- () => state()?.loaded,
- (loaded) => {
- if (!loaded) return
- requestAnimationFrame(restoreScroll)
- },
- { defer: true },
- ),
- )
-
- createEffect(
- on(
- () => file.ready(),
- (ready) => {
- if (!ready) return
- requestAnimationFrame(restoreScroll)
- },
- { defer: true },
- ),
- )
+ let prev = {
+ loaded: false,
+ ready: false,
+ active: false,
+ }
- createEffect(
- on(
- () => tabs().active() === props.tab,
- (active) => {
- if (!active) return
- if (!state()?.loaded) return
- requestAnimationFrame(restoreScroll)
- },
- ),
- )
+ createEffect(() => {
+ const loaded = !!state()?.loaded
+ const ready = file.ready()
+ const active = tabs().active() === props.tab
+ const restore = (loaded && !prev.loaded) || (ready && !prev.ready) || (active && loaded && !prev.active)
+ prev = { loaded, ready, active }
+ if (!restore) return
+ queueRestore()
+ })
onCleanup(() => {
for (const item of codeScroll) {
item.removeEventListener("scroll", handleCodeScroll)
}
- if (scrollFrame === undefined) return
- cancelAnimationFrame(scrollFrame)
+ if (scrollFrame !== undefined) cancelAnimationFrame(scrollFrame)
+ if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
})
const renderFile = (source: string) => (
@@ -421,7 +414,7 @@ export function FileTabContent(props: { tab: string }) {
selectedLines={activeSelection()}
commentedLines={commentedLines()}
onRendered={() => {
- requestAnimationFrame(restoreScroll)
+ queueRestore()
}}
annotations={commentsUi.annotations()}
renderAnnotation={commentsUi.renderAnnotation}
@@ -440,7 +433,7 @@ export function FileTabContent(props: { tab: string }) {
mode: "auto",
path: path(),
current: state()?.content,
- onLoad: () => requestAnimationFrame(restoreScroll),
+ onLoad: queueRestore,
onError: (args: { kind: "image" | "audio" | "svg" }) => {
if (args.kind !== "svg") return
showToast({
diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx
index 2b7eba324..142ee7ad9 100644
--- a/packages/app/src/pages/session/review-tab.tsx
+++ b/packages/app/src/pages/session/review-tab.tsx
@@ -1,4 +1,4 @@
-import { createEffect, on, onCleanup, type JSX } from "solid-js"
+import { createEffect, onCleanup, type JSX } from "solid-js"
import type { FileDiff } from "@opencode-ai/sdk/v2"
import { SessionReview } from "@opencode-ai/ui/session-review"
import type {
@@ -119,32 +119,12 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
})
}
- createEffect(
- on(
- () => props.diffs().length,
- () => queueRestore(),
- { defer: true },
- ),
- )
-
- createEffect(
- on(
- () => props.diffStyle,
- () => queueRestore(),
- { defer: true },
- ),
- )
-
- createEffect(
- on(
- () => layout.ready(),
- (ready) => {
- if (!ready) return
- queueRestore()
- },
- { defer: true },
- ),
- )
+ createEffect(() => {
+ props.diffs().length
+ props.diffStyle
+ if (!layout.ready()) return
+ queueRestore()
+ })
onCleanup(() => {
if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx
index 27ea4e6f3..49bed9490 100644
--- a/packages/app/src/pages/session/terminal-panel.tsx
+++ b/packages/app/src/pages/session/terminal-panel.tsx
@@ -56,9 +56,9 @@ export function TerminalPanel() {
on(
() => terminal.all().length,
(count, prevCount) => {
- if (prevCount !== undefined && prevCount > 0 && count === 0) {
- if (opened()) view().terminal.toggle()
- }
+ if (prevCount === undefined || prevCount <= 0 || count !== 0) return
+ if (!opened()) return
+ close()
},
),
)
diff --git a/packages/app/src/pages/session/use-session-hash-scroll.ts b/packages/app/src/pages/session/use-session-hash-scroll.ts
index 235714588..c5a7dde9e 100644
--- a/packages/app/src/pages/session/use-session-hash-scroll.ts
+++ b/packages/app/src/pages/session/use-session-hash-scroll.ts
@@ -1,4 +1,4 @@
-import { createEffect, createMemo, on, onCleanup } from "solid-js"
+import { createEffect, createMemo, onCleanup, onMount } from "solid-js"
import { UserMessage } from "@opencode-ai/sdk/v2"
export const messageIdFromHash = (hash: string) => {
@@ -28,6 +28,7 @@ export const useSessionHashScroll = (input: {
const visibleUserMessages = createMemo(() => input.visibleUserMessages())
const messageById = createMemo(() => new Map(visibleUserMessages().map((m) => [m.id, m])))
const messageIndex = createMemo(() => new Map(visibleUserMessages().map((m, i) => [m.id, i])))
+ let pendingKey = ""
const clearMessageHash = () => {
if (!window.location.hash) return
@@ -130,15 +131,6 @@ export const useSessionHashScroll = (input: {
if (el) input.scheduleScrollState(el)
}
- createEffect(
- on(input.sessionKey, (key) => {
- if (!input.sessionID()) return
- const messageID = input.consumePendingMessage(key)
- if (!messageID) return
- input.setPendingMessage(messageID)
- }),
- )
-
createEffect(() => {
if (!input.sessionID() || !input.messagesReady()) return
requestAnimationFrame(() => applyHash("auto"))
@@ -150,7 +142,20 @@ export const useSessionHashScroll = (input: {
visibleUserMessages()
input.turnStart()
- const targetId = input.pendingMessage() ?? messageIdFromHash(window.location.hash)
+ let targetId = input.pendingMessage()
+ if (!targetId) {
+ const key = input.sessionKey()
+ if (pendingKey !== key) {
+ pendingKey = key
+ const next = input.consumePendingMessage(key)
+ if (next) {
+ input.setPendingMessage(next)
+ targetId = next
+ }
+ }
+ }
+
+ if (!targetId) targetId = messageIdFromHash(window.location.hash)
if (!targetId) return
if (input.currentMessageId() === targetId) return
@@ -162,9 +167,12 @@ export const useSessionHashScroll = (input: {
requestAnimationFrame(() => scrollToMessage(msg, "auto"))
})
- createEffect(() => {
- if (!input.sessionID() || !input.messagesReady()) return
- const handler = () => requestAnimationFrame(() => applyHash("auto"))
+ onMount(() => {
+ const handler = () => {
+ if (!input.sessionID() || !input.messagesReady()) return
+ requestAnimationFrame(() => applyHash("auto"))
+ }
+
window.addEventListener("hashchange", handler)
onCleanup(() => window.removeEventListener("hashchange", handler))
})