diff options
| author | Adam <[email protected]> | 2026-02-12 11:26:19 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-02-12 11:26:19 -0600 |
| commit | da952135cabba2926698298797cd301e7adaf48c (patch) | |
| tree | 78635fe4f7d656266ad3cc1c353b04b56969515c /packages/app/src/pages | |
| parent | 789705ea96ae28af7e30801fd6039ce89b6ac48e (diff) | |
| download | opencode-da952135cabba2926698298797cd301e7adaf48c.tar.gz opencode-da952135cabba2926698298797cd301e7adaf48c.zip | |
chore(app): refactor for better solidjs hygiene (#13344)
Diffstat (limited to 'packages/app/src/pages')
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 83 | ||||
| -rw-r--r-- | packages/app/src/pages/layout/sidebar-project.tsx | 5 | ||||
| -rw-r--r-- | packages/app/src/pages/layout/sidebar-workspace.tsx | 6 | ||||
| -rw-r--r-- | packages/app/src/pages/session/file-tabs.tsx | 27 | ||||
| -rw-r--r-- | packages/app/src/pages/session/helpers.ts | 9 | ||||
| -rw-r--r-- | packages/app/src/pages/session/session-side-panel.tsx | 6 | ||||
| -rw-r--r-- | packages/app/src/pages/session/terminal-panel.tsx | 16 | ||||
| -rw-r--r-- | packages/app/src/pages/session/use-session-hash-scroll.ts | 17 |
8 files changed, 126 insertions, 43 deletions
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index aca52564b..5f001177f 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -2,6 +2,7 @@ import { batch, createEffect, createMemo, + createSignal, For, on, onCleanup, @@ -124,7 +125,7 @@ export default function Layout(props: ParentProps) { const [state, setState] = createStore({ autoselect: !initialDirectory, - busyWorkspaces: new Set<string>(), + busyWorkspaces: {} as Record<string, boolean>, hoverSession: undefined as string | undefined, hoverProject: undefined as string | undefined, scrollSessionKey: undefined as string | undefined, @@ -134,15 +135,28 @@ export default function Layout(props: ParentProps) { const editor = createInlineEditorController() const setBusy = (directory: string, value: boolean) => { const key = workspaceKey(directory) - setState("busyWorkspaces", (prev) => { - const next = new Set(prev) - if (value) next.add(key) - else next.delete(key) - return next - }) + if (value) { + setState("busyWorkspaces", key, true) + return + } + setState( + "busyWorkspaces", + produce((draft) => { + delete draft[key] + }), + ) } - const isBusy = (directory: string) => state.busyWorkspaces.has(workspaceKey(directory)) + const isBusy = (directory: string) => !!state.busyWorkspaces[workspaceKey(directory)] const navLeave = { current: undefined as number | undefined } + const [sortNow, setSortNow] = createSignal(Date.now()) + let sortNowInterval: ReturnType<typeof setInterval> | undefined + const sortNowTimeout = setTimeout( + () => { + setSortNow(Date.now()) + sortNowInterval = setInterval(() => setSortNow(Date.now()), 60_000) + }, + 60_000 - (Date.now() % 60_000), + ) const aim = createAim({ enabled: () => !layout.sidebar.opened(), @@ -157,6 +171,8 @@ export default function Layout(props: ParentProps) { onCleanup(() => { if (navLeave.current !== undefined) clearTimeout(navLeave.current) + clearTimeout(sortNowTimeout) + if (sortNowInterval) clearInterval(sortNowInterval) aim.reset() }) @@ -518,10 +534,13 @@ export default function Layout(props: ParentProps) { const setWorkspaceName = (directory: string, next: string, projectId?: string, branch?: string) => { const key = workspaceKey(directory) - setStore("workspaceName", (prev) => ({ ...(prev ?? {}), [key]: next })) + setStore("workspaceName", key, next) if (!projectId) return if (!branch) return - setStore("workspaceBranchName", projectId, (prev) => ({ ...(prev ?? {}), [branch]: next })) + if (!store.workspaceBranchName[projectId]) { + setStore("workspaceBranchName", projectId, {}) + } + setStore("workspaceBranchName", projectId, branch, next) } const workspaceLabel = (directory: string, branch?: string, projectId?: string) => @@ -1447,23 +1466,41 @@ export default function Layout(props: ParentProps) { document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`) }) + const loadedSessionDirs = new Set<string>() + createEffect(() => { const project = currentProject() - if (!project) return + const workspaces = workspaceSetting() + const next = new Set<string>() + if (!project) { + loadedSessionDirs.clear() + return + } - if (workspaceSetting()) { + 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 - globalSync.project.loadSessions(directory) + next.add(directory) } - return } - globalSync.project.loadSessions(project.worktree) + if (!workspaces) { + next.add(project.worktree) + } + + for (const directory of next) { + if (loadedSessionDirs.has(directory)) continue + globalSync.project.loadSessions(directory) + } + + loadedSessionDirs.clear() + for (const directory of next) { + loadedSessionDirs.add(directory) + } }) function handleDragStart(event: unknown) { @@ -1766,7 +1803,12 @@ export default function Layout(props: ParentProps) { </TooltipKeybind> </div> <div class="flex-1 min-h-0"> - <LocalWorkspace ctx={workspaceSidebarCtx} project={p()} mobile={panelProps.mobile} /> + <LocalWorkspace + ctx={workspaceSidebarCtx} + project={p()} + sortNow={sortNow} + mobile={panelProps.mobile} + /> </div> </> } @@ -1805,6 +1847,7 @@ export default function Layout(props: ParentProps) { ctx={workspaceSidebarCtx} directory={directory} project={p()} + sortNow={sortNow} mobile={panelProps.mobile} /> )} @@ -1890,7 +1933,9 @@ export default function Layout(props: ParentProps) { opened={() => layout.sidebar.opened()} aimMove={aim.move} projects={() => layout.projects.list()} - renderProject={(project) => <SortableProject ctx={projectSidebarCtx} project={project} />} + renderProject={(project) => ( + <SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} /> + )} handleDragStart={handleDragStart} handleDragEnd={handleDragEnd} handleDragOver={handleDragOver} @@ -1953,7 +1998,9 @@ export default function Layout(props: ParentProps) { opened={() => layout.sidebar.opened()} aimMove={aim.move} projects={() => layout.projects.list()} - renderProject={(project) => <SortableProject ctx={projectSidebarCtx} project={project} mobile />} + renderProject={(project) => ( + <SortableProject ctx={projectSidebarCtx} project={project} sortNow={sortNow} mobile /> + )} handleDragStart={handleDragStart} handleDragEnd={handleDragEnd} handleDragOver={handleDragOver} diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx index 9afa205b6..931053616 100644 --- a/packages/app/src/pages/layout/sidebar-project.tsx +++ b/packages/app/src/pages/layout/sidebar-project.tsx @@ -244,6 +244,7 @@ export const SortableProject = (props: { project: LocalProject mobile?: boolean ctx: ProjectSidebarContext + sortNow: Accessor<number> }): JSX.Element => { const globalSync = useGlobalSync() const language = useLanguage() @@ -284,11 +285,11 @@ export const SortableProject = (props: { } const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0]) - const projectSessions = createMemo(() => sortedRootSessions(projectStore(), Date.now()).slice(0, 2)) + const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()).slice(0, 2)) const projectChildren = createMemo(() => childMapByParent(projectStore().session)) const workspaceSessions = (directory: string) => { const [data] = globalSync.child(directory, { bootstrap: false }) - return sortedRootSessions(data, Date.now()).slice(0, 2) + return sortedRootSessions(data, props.sortNow()).slice(0, 2) } const workspaceChildren = (directory: string) => { const [data] = globalSync.child(directory, { bootstrap: false }) diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index 1d9c2e685..43d99cf89 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -302,6 +302,7 @@ export const SortableWorkspace = (props: { ctx: WorkspaceSidebarContext directory: string project: LocalProject + sortNow: Accessor<number> mobile?: boolean }): JSX.Element => { const navigate = useNavigate() @@ -315,7 +316,7 @@ export const SortableWorkspace = (props: { pendingRename: false, }) const slug = createMemo(() => base64Encode(props.directory)) - const sessions = createMemo(() => sortedRootSessions(workspaceStore, Date.now())) + const sessions = createMemo(() => sortedRootSessions(workspaceStore, props.sortNow())) const children = createMemo(() => childMapByParent(workspaceStore.session)) const local = createMemo(() => props.directory === props.project.worktree) const active = createMemo(() => props.ctx.currentDir() === props.directory) @@ -464,6 +465,7 @@ export const SortableWorkspace = (props: { export const LocalWorkspace = (props: { ctx: WorkspaceSidebarContext project: LocalProject + sortNow: Accessor<number> mobile?: boolean }): JSX.Element => { const globalSync = useGlobalSync() @@ -473,7 +475,7 @@ export const LocalWorkspace = (props: { return { store, setStore } }) const slug = createMemo(() => base64Encode(props.project.worktree)) - const sessions = createMemo(() => sortedRootSessions(workspace().store, Date.now())) + const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow())) const children = createMemo(() => childMapByParent(workspace().store.session)) const booted = createMemo((prev) => prev || workspace().store.status === "complete", false) const loading = createMemo(() => !booted() && sessions().length === 0) diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx index c94c0ff35..5b3f57dbe 100644 --- a/packages/app/src/pages/session/file-tabs.tsx +++ b/packages/app/src/pages/session/file-tabs.tsx @@ -1,5 +1,5 @@ import { type ValidComponent, createEffect, createMemo, For, Match, on, onCleanup, Show, Switch } from "solid-js" -import { createStore } from "solid-js/store" +import { createStore, produce } from "solid-js/store" import { Dynamic } from "solid-js/web" import { checksum } from "@opencode-ai/util/encode" import { decode64 } from "@/utils/base64" @@ -112,6 +112,12 @@ export function FileTabContent(props: { return props.comments.list(p) }) + const commentLayout = createMemo(() => { + return fileComments() + .map((comment) => `${comment.id}:${comment.selection.start}:${comment.selection.end}`) + .join("|") + }) + const commentedLines = createMemo(() => fileComments().map((comment) => comment.selection)) const [note, setNote] = createStore({ @@ -164,7 +170,22 @@ export function FileTabContent(props: { next[comment.id] = markerTop(el, marker) } - setNote("positions", next) + const removed = Object.keys(note.positions).filter((id) => next[id] === undefined) + const changed = Object.entries(next).filter(([id, top]) => note.positions[id] !== top) + if (removed.length > 0 || changed.length > 0) { + setNote( + "positions", + produce((draft) => { + for (const id of removed) { + delete draft[id] + } + + for (const [id, top] of changed) { + draft[id] = top + } + }), + ) + } const range = note.commenting if (!range) { @@ -186,7 +207,7 @@ export function FileTabContent(props: { } createEffect(() => { - fileComments() + commentLayout() scheduleComments() }) diff --git a/packages/app/src/pages/session/helpers.ts b/packages/app/src/pages/session/helpers.ts index dcf2c8784..6ead7a7f2 100644 --- a/packages/app/src/pages/session/helpers.ts +++ b/packages/app/src/pages/session/helpers.ts @@ -1,4 +1,5 @@ import type { CommandOption } from "@/context/command" +import { batch } from "solid-js" export const focusTerminalById = (id: string) => { const wrapper = document.getElementById(`terminal-wrapper-${id}`) @@ -27,9 +28,11 @@ export const createOpenReviewFile = (input: { loadFile: (path: string) => void }) => { return (path: string) => { - input.showAllFiles() - input.openTab(input.tabForPath(path)) - input.loadFile(path) + batch(() => { + input.showAllFiles() + input.openTab(input.tabForPath(path)) + input.loadFile(path) + }) } } diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 15ad90ffe..33954f64a 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -72,6 +72,8 @@ export function SessionSidePanel(props: { activeDiff?: string focusReviewDiff: (path: string) => void }) { + const openedTabs = createMemo(() => props.openedTabs()) + return ( <Show when={props.open}> <aside @@ -140,8 +142,8 @@ export function SessionSidePanel(props: { </div> </Tabs.Trigger> </Show> - <SortableProvider ids={props.openedTabs()}> - <For each={props.openedTabs()}> + <SortableProvider ids={openedTabs()}> + <For each={openedTabs()}> {(tab) => <SortableTab tab={tab} onTabClose={props.tabs().close} />} </For> </SortableProvider> diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index d3475c714..7ec4356b1 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -1,4 +1,4 @@ -import { For, Show } from "solid-js" +import { For, Show, createMemo } from "solid-js" import { Tabs } from "@opencode-ai/ui/tabs" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { IconButton } from "@opencode-ai/ui/icon-button" @@ -8,7 +8,7 @@ import type { DragEvent } from "@thisbeyond/solid-dnd" import { ConstrainDragYAxis } from "@/utils/solid-dnd" import { SortableTerminalTab } from "@/components/session" import { Terminal } from "@/components/terminal" -import { useTerminal, type LocalPTY } from "@/context/terminal" +import { useTerminal } from "@/context/terminal" import { useLanguage } from "@/context/language" import { useCommand } from "@/context/command" import { terminalTabLabel } from "@/pages/session/terminal-label" @@ -28,6 +28,10 @@ export function TerminalPanel(props: { handleTerminalDragEnd: () => void onCloseTab: () => void }) { + const all = createMemo(() => props.terminal.all()) + const ids = createMemo(() => all().map((pty) => pty.id)) + const byId = createMemo(() => new Map(all().map((pty) => [pty.id, pty]))) + return ( <Show when={props.open}> <div @@ -86,8 +90,8 @@ export function TerminalPanel(props: { class="!h-auto !flex-none" > <Tabs.List class="h-10"> - <SortableProvider ids={props.terminal.all().map((t: LocalPTY) => t.id)}> - <For each={props.terminal.all()}> + <SortableProvider ids={ids()}> + <For each={all()}> {(pty) => ( <SortableTerminalTab terminal={pty} @@ -117,7 +121,7 @@ export function TerminalPanel(props: { </Tabs.List> </Tabs> <div class="flex-1 min-h-0 relative"> - <For each={props.terminal.all()}> + <For each={all()}> {(pty) => ( <div id={`terminal-wrapper-${pty.id}`} @@ -142,7 +146,7 @@ export function TerminalPanel(props: { <Show when={props.activeTerminalDraggable()}> {(draggedId) => { return ( - <Show when={props.terminal.all().find((t: LocalPTY) => t.id === draggedId())}> + <Show when={byId().get(draggedId())}> {(t) => ( <div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular"> {terminalTabLabel({ 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 8952bbd98..555761ad1 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, on, onCleanup } from "solid-js" +import { createEffect, createMemo, on, onCleanup } from "solid-js" import { UserMessage } from "@opencode-ai/sdk/v2" export const messageIdFromHash = (hash: string) => { @@ -26,6 +26,10 @@ export const useSessionHashScroll = (input: { scheduleScrollState: (el: HTMLDivElement) => void consumePendingMessage: (key: string) => string | undefined }) => { + 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]))) + const clearMessageHash = () => { if (!window.location.hash) return window.history.replaceState(null, "", window.location.href.replace(/#.*$/, "")) @@ -47,10 +51,9 @@ export const useSessionHashScroll = (input: { } const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => { - input.setActiveMessage(message) + if (input.currentMessageId() !== message.id) input.setActiveMessage(message) - const msgs = input.visibleUserMessages() - const index = msgs.findIndex((m) => m.id === message.id) + const index = messageIndex().get(message.id) ?? -1 if (index !== -1 && index < input.turnStart()) { input.setTurnStart(index) input.scheduleTurnBackfill() @@ -107,7 +110,7 @@ export const useSessionHashScroll = (input: { const messageId = messageIdFromHash(hash) if (messageId) { input.autoScroll.pause() - const msg = input.visibleUserMessages().find((m) => m.id === messageId) + const msg = messageById().get(messageId) if (msg) { scrollToMessage(msg, behavior) return @@ -144,14 +147,14 @@ export const useSessionHashScroll = (input: { createEffect(() => { if (!input.sessionID() || !input.messagesReady()) return - input.visibleUserMessages().length + visibleUserMessages() input.turnStart() const targetId = input.pendingMessage() ?? messageIdFromHash(window.location.hash) if (!targetId) return if (input.currentMessageId() === targetId) return - const msg = input.visibleUserMessages().find((m) => m.id === targetId) + const msg = messageById().get(targetId) if (!msg) return if (input.pendingMessage() === targetId) input.setPendingMessage(undefined) |
