From da952135cabba2926698298797cd301e7adaf48c Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:26:19 -0600 Subject: chore(app): refactor for better solidjs hygiene (#13344) --- packages/app/src/pages/layout.tsx | 83 +++++++++++++++++----- packages/app/src/pages/layout/sidebar-project.tsx | 5 +- .../app/src/pages/layout/sidebar-workspace.tsx | 6 +- packages/app/src/pages/session/file-tabs.tsx | 27 ++++++- packages/app/src/pages/session/helpers.ts | 9 ++- .../app/src/pages/session/session-side-panel.tsx | 6 +- packages/app/src/pages/session/terminal-panel.tsx | 16 +++-- .../src/pages/session/use-session-hash-scroll.ts | 17 +++-- 8 files changed, 126 insertions(+), 43 deletions(-) (limited to 'packages/app/src/pages') 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(), + busyWorkspaces: {} as Record, 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 | 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() + createEffect(() => { const project = currentProject() - if (!project) return + const workspaces = workspaceSetting() + const next = new Set() + 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) {
- +
} @@ -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) => } + renderProject={(project) => ( + + )} 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) => } + renderProject={(project) => ( + + )} 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 }): 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 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 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 (