chore(app): refactor for better solidjs hygiene (#13344)
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user