From d05ed5ca83e03904c7c3dda23a6dcccfd607bdce Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:04:59 -0600 Subject: chore(app): createStore over signals --- packages/app/src/pages/layout.tsx | 147 ++++++++++++++++++++----------------- packages/app/src/pages/session.tsx | 99 +++++++++++++++++-------- 2 files changed, 147 insertions(+), 99 deletions(-) (limited to 'packages/app/src/pages') diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 7ad97a989..fd6f6e527 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -91,7 +91,6 @@ export default function Layout(props: ParentProps) { let scrollContainerRef: HTMLDivElement | undefined const params = useParams() - const [autoselect, setAutoselect] = createSignal(!params.dir) const globalSDK = useGlobalSDK() const globalSync = useGlobalSync() const layout = useLayout() @@ -117,27 +116,31 @@ export default function Layout(props: ParentProps) { } const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme]) + const [state, setState] = createStore({ + autoselect: !params.dir, + busyWorkspaces: new Set(), + hoverSession: undefined as string | undefined, + hoverProject: undefined as string | undefined, + scrollSessionKey: undefined as string | undefined, + nav: undefined as HTMLElement | undefined, + }) + const [editor, setEditor] = createStore({ active: "" as string, value: "", }) - const [busyWorkspaces, setBusyWorkspaces] = createSignal>(new Set()) const setBusy = (directory: string, value: boolean) => { const key = workspaceKey(directory) - setBusyWorkspaces((prev) => { + setState("busyWorkspaces", (prev) => { const next = new Set(prev) if (value) next.add(key) else next.delete(key) return next }) } - const isBusy = (directory: string) => busyWorkspaces().has(workspaceKey(directory)) + const isBusy = (directory: string) => state.busyWorkspaces.has(workspaceKey(directory)) const editorRef = { current: undefined as HTMLInputElement | undefined } - const [hoverSession, setHoverSession] = createSignal() - const [hoverProject, setHoverProject] = createSignal() - - const [nav, setNav] = createSignal(undefined) const navLeave = { current: undefined as number | undefined } onCleanup(() => { @@ -145,18 +148,18 @@ export default function Layout(props: ParentProps) { clearTimeout(navLeave.current) }) - const sidebarHovering = createMemo(() => !layout.sidebar.opened() && hoverProject() !== undefined) + const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined) const sidebarExpanded = createMemo(() => layout.sidebar.opened() || sidebarHovering()) const hoverProjectData = createMemo(() => { - const id = hoverProject() + const id = state.hoverProject if (!id) return return layout.projects.list().find((project) => project.worktree === id) }) createEffect(() => { if (!layout.sidebar.opened()) return - setHoverProject(undefined) + setState("hoverProject", undefined) }) createEffect( @@ -164,9 +167,9 @@ export default function Layout(props: ParentProps) { () => ({ dir: params.dir, id: params.id }), () => { if (layout.sidebar.opened()) return - if (!hoverProject()) return - setHoverSession(undefined) - setHoverProject(undefined) + if (!state.hoverProject) return + setState("hoverSession", undefined) + setState("hoverProject", undefined) }, { defer: true }, ), @@ -175,7 +178,7 @@ export default function Layout(props: ParentProps) { const autoselecting = createMemo(() => { if (params.dir) return false if (initialDir) return false - if (!autoselect()) return false + if (!state.autoselect) return false if (!pageReady()) return true if (!layoutReady()) return true const list = layout.projects.list() @@ -483,20 +486,18 @@ export default function Layout(props: ParentProps) { } } - const [scrollSessionKey, setScrollSessionKey] = createSignal(undefined) - function scrollToSession(sessionId: string, sessionKey: string) { if (!scrollContainerRef) return - if (scrollSessionKey() === sessionKey) return + if (state.scrollSessionKey === sessionKey) return const element = scrollContainerRef.querySelector(`[data-session-id="${sessionId}"]`) if (!element) return const containerRect = scrollContainerRef.getBoundingClientRect() const elementRect = element.getBoundingClientRect() if (elementRect.top >= containerRect.top && elementRect.bottom <= containerRect.bottom) { - setScrollSessionKey(sessionKey) + setState("scrollSessionKey", sessionKey) return } - setScrollSessionKey(sessionKey) + setState("scrollSessionKey", sessionKey) element.scrollIntoView({ block: "nearest", behavior: "smooth" }) } @@ -544,7 +545,7 @@ export default function Layout(props: ParentProps) { (value) => { if (!value.ready) return if (!value.layoutReady) return - if (!autoselect()) return + if (!state.autoselect) return if (initialDir) return if (value.dir) return if (value.list.length === 0) return @@ -552,7 +553,7 @@ export default function Layout(props: ParentProps) { const last = server.projects.last() const next = value.list.find((project) => project.worktree === last) ?? value.list[0] if (!next) return - setAutoselect(false) + setState("autoselect", false) openProject(next.worktree, false) navigateToProject(next.worktree) }, @@ -1066,8 +1067,8 @@ export default function Layout(props: ParentProps) { function navigateToProject(directory: string | undefined) { if (!directory) return if (!layout.sidebar.opened()) { - setHoverSession(undefined) - setHoverProject(undefined) + setState("hoverSession", undefined) + setState("hoverProject", undefined) } server.projects.touch(directory) const lastSession = store.lastSession[directory] @@ -1078,8 +1079,8 @@ export default function Layout(props: ParentProps) { function navigateToSession(session: Session | undefined) { if (!session) return if (!layout.sidebar.opened()) { - setHoverSession(undefined) - setHoverProject(undefined) + setState("hoverSession", undefined) + setState("hoverProject", undefined) } navigate(`/${base64Encode(session.directory)}/session/${session.id}`) layout.mobileSidebar.hide() @@ -1472,7 +1473,7 @@ export default function Layout(props: ParentProps) { function handleDragStart(event: unknown) { const id = getDraggableId(event) if (!id) return - setHoverProject(undefined) + setState("hoverProject", undefined) setStore("activeProject", id) } @@ -1632,8 +1633,10 @@ export default function Layout(props: ParentProps) { const hoverAllowed = createMemo(() => !props.mobile && sidebarExpanded()) const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed()) const isActive = createMemo(() => props.session.id === params.id) - const [menuOpen, setMenuOpen] = createSignal(false) - const [pendingRename, setPendingRename] = createSignal(false) + const [menu, setMenu] = createStore({ + open: false, + pendingRename: false, + }) const messageLabel = (message: Message) => { const parts = sessionStore.part[message.id] ?? [] @@ -1644,13 +1647,13 @@ export default function Layout(props: ParentProps) { const item = ( prefetchSession(props.session, "high")} onFocus={() => prefetchSession(props.session, "high")} onClick={() => { - setHoverSession(undefined) + setState("hoverSession", undefined) if (layout.sidebar.opened()) return - queueMicrotask(() => setHoverProject(undefined)) + queueMicrotask(() => setState("hoverProject", undefined)) }} >
@@ -1713,9 +1716,9 @@ export default function Layout(props: ParentProps) { gutter={16} shift={-2} trigger={item} - mount={!props.mobile ? nav() : undefined} - open={hoverSession() === props.session.id} - onOpenChange={(open) => setHoverSession(open ? props.session.id : undefined)} + mount={!props.mobile ? state.nav : undefined} + open={state.hoverSession === props.session.id} + onOpenChange={(open) => setState("hoverSession", open ? props.session.id : undefined)} > - + setMenu("open", open)}> - + { - if (!pendingRename()) return + if (!menu.pendingRename) return event.preventDefault() - setPendingRename(false) + setMenu("pendingRename", false) openEditor(`session:${props.session.id}`, props.session.title) }} > { - setPendingRename(true) - setMenuOpen(false) + setMenu("pendingRename", true) + setMenu("open", false) }} > {language.t("common.rename")} @@ -1802,9 +1805,9 @@ export default function Layout(props: ParentProps) { end class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`} onClick={() => { - setHoverSession(undefined) + setState("hoverSession", undefined) if (layout.sidebar.opened()) return - queueMicrotask(() => setHoverProject(undefined)) + queueMicrotask(() => setState("hoverProject", undefined)) }} >
@@ -1884,8 +1887,10 @@ export default function Layout(props: ParentProps) { const SortableWorkspace = (props: { directory: string; project: LocalProject; mobile?: boolean }): JSX.Element => { const sortable = createSortable(props.directory) const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory, { bootstrap: false }) - const [menuOpen, setMenuOpen] = createSignal(false) - const [pendingRename, setPendingRename] = createSignal(false) + const [menu, setMenu] = createStore({ + open: false, + pendingRename: false, + }) const slug = createMemo(() => base64Encode(props.directory)) const sessions = createMemo(() => workspaceStore.session @@ -1995,13 +2000,17 @@ export default function Layout(props: ParentProps) {
- + setMenu("open", open)} + > - + { - if (!pendingRename()) return + if (!menu.pendingRename) return event.preventDefault() - setPendingRename(false) + setMenu("pendingRename", false) openEditor(`workspace:${props.directory}`, workspaceValue()) }} > { - setPendingRename(true) - setMenuOpen(false) + setMenu("pendingRename", true) + setMenu("open", false) }} > {language.t("common.rename")} @@ -2103,7 +2112,7 @@ export default function Layout(props: ParentProps) { const preview = createMemo(() => !props.mobile && layout.sidebar.opened()) const overlay = createMemo(() => !props.mobile && !layout.sidebar.opened()) - const active = createMemo(() => (preview() ? open() : overlay() && hoverProject() === props.project.worktree)) + const active = createMemo(() => (preview() ? open() : overlay() && state.hoverProject === props.project.worktree)) createEffect(() => { if (preview()) return @@ -2155,14 +2164,14 @@ export default function Layout(props: ParentProps) { onMouseEnter={() => { if (!overlay()) return globalSync.child(props.project.worktree) - setHoverProject(props.project.worktree) - setHoverSession(undefined) + setState("hoverProject", props.project.worktree) + setState("hoverSession", undefined) }} onFocus={() => { if (!overlay()) return globalSync.child(props.project.worktree) - setHoverProject(props.project.worktree) - setHoverSession(undefined) + setState("hoverProject", props.project.worktree) + setState("hoverSession", undefined) }} onClick={() => navigateToProject(props.project.worktree)} onBlur={() => setOpen(false)} @@ -2184,7 +2193,7 @@ export default function Layout(props: ParentProps) { trigger={trigger} onOpenChange={(value) => { setOpen(value) - if (value) setHoverSession(undefined) + if (value) setState("hoverSession", undefined) }} >
@@ -2323,8 +2332,8 @@ export default function Layout(props: ParentProps) { const createWorkspace = async (project: LocalProject) => { if (!layout.sidebar.opened()) { - setHoverSession(undefined) - setHoverProject(undefined) + setState("hoverSession", undefined) + setState("hoverProject", undefined) } const created = await globalSDK.client.worktree .create({ directory: project.worktree }) @@ -2427,7 +2436,7 @@ export default function Layout(props: ParentProps) { class="shrink-0 size-6 rounded-md opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100 data-[expanded]:bg-surface-base-active" aria-label={language.t("common.moreOptions")} /> - + dialog.show(() => )}> {language.t("common.edit")} @@ -2476,8 +2485,8 @@ export default function Layout(props: ParentProps) { class="w-full" onClick={() => { if (!layout.sidebar.opened()) { - setHoverSession(undefined) - setHoverProject(undefined) + setState("hoverSession", undefined) + setState("hoverProject", undefined) } navigate(`/${base64Encode(p.worktree)}/session`) layout.mobileSidebar.hide() @@ -2668,7 +2677,7 @@ export default function Layout(props: ParentProps) { }} style={{ width: layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "64px" }} ref={(el) => { - setNav(el) + setState("nav", el) }} onMouseEnter={() => { if (navLeave.current === undefined) return @@ -2681,8 +2690,8 @@ export default function Layout(props: ParentProps) { if (navLeave.current !== undefined) clearTimeout(navLeave.current) navLeave.current = window.setTimeout(() => { navLeave.current = undefined - setHoverProject(undefined) - setHoverSession(undefined) + setState("hoverProject", undefined) + setState("hoverSession", undefined) }, 300) }} > diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 458decfc4..e9b29c03e 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1,4 +1,4 @@ -import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on, createSignal } from "solid-js" +import { For, onCleanup, onMount, Show, Match, Switch, createMemo, createEffect, on } from "solid-js" import { createMediaQuery } from "@solid-primitives/media" import { createResizeObserver } from "@solid-primitives/resize-observer" import { Dynamic } from "solid-js/web" @@ -198,12 +198,17 @@ export default function Page() { return next }) - const [responding, setResponding] = createSignal(false) + const [ui, setUi] = createStore({ + responding: false, + pendingMessage: undefined as string | undefined, + scrollGesture: 0, + autoCreated: false, + }) createEffect( on( () => request()?.id, - () => setResponding(false), + () => setUi("responding", false), { defer: true }, ), ) @@ -211,18 +216,17 @@ export default function Page() { const decide = (response: "once" | "always" | "reject") => { const perm = request() if (!perm) return - if (responding()) return + if (ui.responding) return - setResponding(true) + setUi("responding", true) sdk.client.permission .respond({ sessionID: perm.sessionID, permissionID: perm.id, response }) .catch((err: unknown) => { const message = err instanceof Error ? err.message : String(err) showToast({ title: language.t("common.requestFailed"), description: message }) }) - .finally(() => setResponding(false)) + .finally(() => setUi("responding", false)) } - const [pendingMessage, setPendingMessage] = createSignal(undefined) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const tabs = createMemo(() => layout.tabs(sessionKey)) const view = createMemo(() => layout.view(sessionKey)) @@ -439,7 +443,6 @@ export default function Page() { let promptDock: HTMLDivElement | undefined let scroller: HTMLDivElement | undefined - const [scrollGesture, setScrollGesture] = createSignal(0) const scrollGestureWindowMs = 250 const markScrollGesture = (target?: EventTarget | null) => { @@ -450,26 +453,24 @@ export default function Page() { const nested = el?.closest("[data-scrollable]") if (nested && nested !== root) return - setScrollGesture(Date.now()) + setUi("scrollGesture", Date.now()) } - const hasScrollGesture = () => Date.now() - scrollGesture() < scrollGestureWindowMs + const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs createEffect(() => { if (!params.id) return sync.session.sync(params.id) }) - const [autoCreated, setAutoCreated] = createSignal(false) - createEffect(() => { if (!view().terminal.opened()) { - setAutoCreated(false) + setUi("autoCreated", false) return } - if (!terminal.ready() || terminal.all().length !== 0 || autoCreated()) return + if (!terminal.ready() || terminal.all().length !== 0 || ui.autoCreated) return terminal.new() - setAutoCreated(true) + setUi("autoCreated", true) }) createEffect( @@ -1019,9 +1020,18 @@ export default function Page() { const showTabs = createMemo(() => view().reviewPanel.opened()) - const [fileTreeTab, setFileTreeTab] = createSignal<"changes" | "all">("changes") - const [reviewScroll, setReviewScroll] = createSignal(undefined) - const [pendingDiff, setPendingDiff] = createSignal(undefined) + const [tree, setTree] = createStore({ + fileTreeTab: "changes" as "changes" | "all", + reviewScroll: undefined as HTMLDivElement | undefined, + pendingDiff: undefined as string | undefined, + }) + + const fileTreeTab = () => tree.fileTreeTab + const setFileTreeTab = (value: "changes" | "all") => setTree("fileTreeTab", value) + const reviewScroll = () => tree.reviewScroll + const setReviewScroll = (value: HTMLDivElement | undefined) => setTree("reviewScroll", value) + const pendingDiff = () => tree.pendingDiff + const setPendingDiff = (value: string | undefined) => setTree("pendingDiff", value) createEffect(() => { if (!layout.fileTree.opened()) return @@ -1316,7 +1326,7 @@ export default function Page() { if (pendingSessionID !== sessionID) return sessionStorage.removeItem("opencode.pendingMessage") - setPendingMessage(messageID) + setUi("pendingMessage", messageID) }) const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => { @@ -1484,7 +1494,7 @@ export default function Page() { store.turnStart const targetId = - pendingMessage() ?? + ui.pendingMessage ?? (() => { const hash = window.location.hash.slice(1) const match = hash.match(/^message-(.+)$/) @@ -1496,7 +1506,7 @@ export default function Page() { const msg = visibleUserMessages().find((m) => m.id === targetId) if (!msg) return - if (pendingMessage() === targetId) setPendingMessage(undefined) + if (ui.pendingMessage === targetId) setUi("pendingMessage", undefined) requestAnimationFrame(() => scrollToMessage(msg, "auto")) }) @@ -1877,18 +1887,18 @@ export default function Page() {
- -
@@ -2144,11 +2154,40 @@ export default function Page() { const commentedLines = createMemo(() => fileComments().map((comment) => comment.selection)) - const [openedComment, setOpenedComment] = createSignal(null) - const [commenting, setCommenting] = createSignal(null) - const [draft, setDraft] = createSignal("") - const [positions, setPositions] = createSignal>({}) - const [draftTop, setDraftTop] = createSignal(undefined) + const [note, setNote] = createStore({ + openedComment: null as string | null, + commenting: null as SelectedLineRange | null, + draft: "", + positions: {} as Record, + draftTop: undefined as number | undefined, + }) + + const openedComment = () => note.openedComment + const setOpenedComment = ( + value: + | typeof note.openedComment + | ((value: typeof note.openedComment) => typeof note.openedComment), + ) => setNote("openedComment", value) + + const commenting = () => note.commenting + const setCommenting = ( + value: typeof note.commenting | ((value: typeof note.commenting) => typeof note.commenting), + ) => setNote("commenting", value) + + const draft = () => note.draft + const setDraft = ( + value: typeof note.draft | ((value: typeof note.draft) => typeof note.draft), + ) => setNote("draft", value) + + const positions = () => note.positions + const setPositions = ( + value: typeof note.positions | ((value: typeof note.positions) => typeof note.positions), + ) => setNote("positions", value) + + const draftTop = () => note.draftTop + const setDraftTop = ( + value: typeof note.draftTop | ((value: typeof note.draftTop) => typeof note.draftTop), + ) => setNote("draftTop", value) const commentLabel = (range: SelectedLineRange) => { const start = Math.min(range.start, range.end) @@ -2695,7 +2734,7 @@ export default function Page() { terminal={pty} onClose={() => { view().terminal.close() - setAutoCreated(false) + setUi("autoCreated", false) }} /> )} -- cgit v1.2.3