fix(app): sidebar sync

This commit is contained in:
Adam
2026-03-13 10:47:45 -05:00
parent 1cbe7b0854
commit 389daa03df

View File

@@ -1,4 +1,16 @@
import { batch, createEffect, createMemo, For, on, onCleanup, onMount, ParentProps, Show, untrack } from "solid-js" import {
batch,
createEffect,
createMemo,
For,
on,
onCleanup,
onMount,
ParentProps,
Show,
untrack,
type Accessor,
} from "solid-js"
import { useNavigate, useParams } from "@solidjs/router" import { useNavigate, useParams } from "@solidjs/router"
import { useLayout, LocalProject } from "@/context/layout" import { useLayout, LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync" import { useGlobalSync } from "@/context/global-sync"
@@ -135,7 +147,7 @@ export default function Layout(props: ParentProps) {
nav: undefined as HTMLElement | undefined, nav: undefined as HTMLElement | undefined,
sortNow: Date.now(), sortNow: Date.now(),
sizing: false, sizing: false,
peek: undefined as LocalProject | undefined, peek: undefined as string | undefined,
peeked: false, peeked: false,
}) })
@@ -233,6 +245,12 @@ export default function Layout(props: ParentProps) {
return layout.projects.list().find((project) => project.worktree === id) return layout.projects.list().find((project) => project.worktree === id)
}) })
const peekProject = createMemo(() => {
const id = state.peek
if (!id) return
return layout.projects.list().find((project) => project.worktree === id)
})
createEffect(() => { createEffect(() => {
const p = hoverProjectData() const p = hoverProjectData()
if (p) { if (p) {
@@ -240,7 +258,7 @@ export default function Layout(props: ParentProps) {
clearTimeout(peekt) clearTimeout(peekt)
peekt = undefined peekt = undefined
} }
setState("peek", p) setState("peek", p.worktree)
setState("peeked", true) setState("peeked", true)
return return
} }
@@ -1932,17 +1950,32 @@ export default function Layout(props: ParentProps) {
setHoverSession, setHoverSession,
} }
const SidebarPanel = (panelProps: { project: LocalProject | undefined; mobile?: boolean; merged?: boolean }) => { const SidebarPanel = (panelProps: {
project: Accessor<LocalProject | undefined>
mobile?: boolean
merged?: boolean
}) => {
const project = panelProps.project
const merged = createMemo(() => panelProps.mobile || (panelProps.merged ?? layout.sidebar.opened())) const merged = createMemo(() => panelProps.mobile || (panelProps.merged ?? layout.sidebar.opened()))
const hover = createMemo(() => !panelProps.mobile && panelProps.merged === false && !layout.sidebar.opened()) const hover = createMemo(() => !panelProps.mobile && panelProps.merged === false && !layout.sidebar.opened())
const popover = createMemo(() => !!panelProps.mobile || panelProps.merged === false || layout.sidebar.opened()) const popover = createMemo(() => !!panelProps.mobile || panelProps.merged === false || layout.sidebar.opened())
const projectName = createMemo(() => { const projectName = createMemo(() => {
const project = panelProps.project const item = project()
if (!project) return "" if (!item) return ""
return project.name || getFilename(project.worktree) return item.name || getFilename(item.worktree)
})
const projectId = createMemo(() => project()?.id ?? "")
const worktree = createMemo(() => project()?.worktree ?? "")
const slug = createMemo(() => {
const dir = worktree()
if (!dir) return ""
return base64Encode(dir)
})
const workspaces = createMemo(() => {
const item = project()
if (!item) return [] as string[]
return workspaceIds(item)
}) })
const projectId = createMemo(() => panelProps.project?.id ?? "")
const workspaces = createMemo(() => workspaceIds(panelProps.project))
const unseenCount = createMemo(() => const unseenCount = createMemo(() =>
workspaces().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), workspaces().reduce((total, directory) => total + notification.project.unseenCount(directory), 0),
) )
@@ -1951,10 +1984,15 @@ export default function Layout(props: ParentProps) {
.filter((directory) => notification.project.unseenCount(directory) > 0) .filter((directory) => notification.project.unseenCount(directory) > 0)
.forEach((directory) => notification.project.markViewed(directory)) .forEach((directory) => notification.project.markViewed(directory))
const workspacesEnabled = createMemo(() => { const workspacesEnabled = createMemo(() => {
const project = panelProps.project const item = project()
if (!project) return false if (!item) return false
if (project.vcs !== "git") return false if (item.vcs !== "git") return false
return layout.sidebar.workspaces(project.worktree)() return layout.sidebar.workspaces(item.worktree)()
})
const canToggle = createMemo(() => {
const item = project()
if (!item) return false
return item.vcs === "git" || layout.sidebar.workspaces(item.worktree)()
}) })
const homedir = createMemo(() => globalSync.data.path.home) const homedir = createMemo(() => globalSync.data.path.home)
@@ -1973,168 +2011,197 @@ export default function Layout(props: ParentProps) {
width: panelProps.mobile ? undefined : `${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px`, width: panelProps.mobile ? undefined : `${Math.max(Math.max(layout.sidebar.width(), 244) - 64, 0)}px`,
}} }}
> >
<Show when={panelProps.project}> <Show when={project()}>
{(p) => ( <>
<> <div class="shrink-0 pl-1 py-1">
<div class="shrink-0 pl-1 py-1"> <div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0">
<div class="group/project flex items-start justify-between gap-2 py-2 pl-2 pr-0"> <div class="flex flex-col min-w-0">
<div class="flex flex-col min-w-0"> <InlineEditor
<InlineEditor id={`project:${projectId()}`}
id={`project:${projectId()}`} value={projectName}
value={projectName} onSave={(next) => {
onSave={(next) => renameProject(p(), next)} const item = project()
class="text-14-medium text-text-strong truncate" if (!item) return
displayClass="text-14-medium text-text-strong truncate" renameProject(item, next)
stopPropagation }}
/> class="text-14-medium text-text-strong truncate"
displayClass="text-14-medium text-text-strong truncate"
stopPropagation
/>
<Tooltip <Tooltip
placement="bottom" placement="bottom"
gutter={2} gutter={2}
value={p().worktree} value={worktree()}
class="shrink-0" class="shrink-0"
contentStyle={{ contentStyle={{
"max-width": "640px", "max-width": "640px",
transform: "translate3d(52px, 0, 0)", transform: "translate3d(52px, 0, 0)",
}} }}
> >
<span class="text-12-regular text-text-base truncate select-text"> <span class="text-12-regular text-text-base truncate select-text">
{p().worktree.replace(homedir(), "~")} {worktree().replace(homedir(), "~")}
</span> </span>
</Tooltip> </Tooltip>
</div>
<DropdownMenu modal={!sidebarHovering()}>
<DropdownMenu.Trigger
as={IconButton}
icon="dot-grid"
variant="ghost"
data-action="project-menu"
data-project={base64Encode(p().worktree)}
class="shrink-0 size-6 rounded-md data-[expanded]:bg-surface-base-active"
classList={{
"opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100": !panelProps.mobile,
}}
aria-label={language.t("common.moreOptions")}
/>
<DropdownMenu.Portal>
<DropdownMenu.Content class="mt-1">
<DropdownMenu.Item onSelect={() => showEditProjectDialog(p())}>
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item
data-action="project-workspaces-toggle"
data-project={base64Encode(p().worktree)}
disabled={p().vcs !== "git" && !layout.sidebar.workspaces(p().worktree)()}
onSelect={() => toggleProjectWorkspaces(p())}
>
<DropdownMenu.ItemLabel>
{layout.sidebar.workspaces(p().worktree)()
? language.t("sidebar.workspaces.disable")
: language.t("sidebar.workspaces.enable")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item
data-action="project-clear-notifications"
data-project={base64Encode(p().worktree)}
disabled={unseenCount() === 0}
onSelect={clearNotifications}
>
<DropdownMenu.ItemLabel>
{language.t("sidebar.project.clearNotifications")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item
data-action="project-close-menu"
data-project={base64Encode(p().worktree)}
onSelect={() => closeProject(p().worktree)}
>
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</div> </div>
</div>
<div class="flex-1 min-h-0 flex flex-col"> <DropdownMenu modal={!sidebarHovering()}>
<Show <DropdownMenu.Trigger
when={workspacesEnabled()} as={IconButton}
fallback={ icon="dot-grid"
<> variant="ghost"
<div class="shrink-0 py-4"> data-action="project-menu"
<Button data-project={slug()}
size="large" class="shrink-0 size-6 rounded-md data-[expanded]:bg-surface-base-active"
icon="new-session" classList={{
class="w-full" "opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100": !panelProps.mobile,
onClick={() => navigateWithSidebarReset(`/${base64Encode(p().worktree)}/session`)} }}
> aria-label={language.t("common.moreOptions")}
{language.t("command.session.new")} />
</Button> <DropdownMenu.Portal>
</div> <DropdownMenu.Content class="mt-1">
<div class="flex-1 min-h-0"> <DropdownMenu.Item
<LocalWorkspace onSelect={() => {
ctx={workspaceSidebarCtx} const item = project()
project={p()} if (!item) return
sortNow={sortNow} showEditProjectDialog(item)
mobile={panelProps.mobile} }}
popover={popover()} >
/> <DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
</div> </DropdownMenu.Item>
</> <DropdownMenu.Item
} data-action="project-workspaces-toggle"
> data-project={slug()}
disabled={!canToggle()}
onSelect={() => {
const item = project()
if (!item) return
toggleProjectWorkspaces(item)
}}
>
<DropdownMenu.ItemLabel>
{workspacesEnabled()
? language.t("sidebar.workspaces.disable")
: language.t("sidebar.workspaces.enable")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item
data-action="project-clear-notifications"
data-project={slug()}
disabled={unseenCount() === 0}
onSelect={clearNotifications}
>
<DropdownMenu.ItemLabel>
{language.t("sidebar.project.clearNotifications")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item
data-action="project-close-menu"
data-project={slug()}
onSelect={() => {
const dir = worktree()
if (!dir) return
closeProject(dir)
}}
>
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</div>
</div>
<div class="flex-1 min-h-0 flex flex-col">
<Show
when={workspacesEnabled()}
fallback={
<> <>
<div class="shrink-0 py-4"> <div class="shrink-0 py-4">
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p())}> <Button
{language.t("workspace.new")} size="large"
icon="new-session"
class="w-full"
onClick={() => {
const dir = worktree()
if (!dir) return
navigateWithSidebarReset(`/${base64Encode(dir)}/session`)
}}
>
{language.t("command.session.new")}
</Button> </Button>
</div> </div>
<div class="relative flex-1 min-h-0"> <div class="flex-1 min-h-0">
<DragDropProvider <LocalWorkspace
onDragStart={handleWorkspaceDragStart} ctx={workspaceSidebarCtx}
onDragEnd={handleWorkspaceDragEnd} project={project()!}
onDragOver={handleWorkspaceDragOver} sortNow={sortNow}
collisionDetector={closestCenter} mobile={panelProps.mobile}
> popover={popover()}
<DragDropSensors /> />
<ConstrainDragXAxis />
<div
ref={(el) => {
if (!panelProps.mobile) scrollContainerRef = el
}}
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
>
<SortableProvider ids={workspaces()}>
<For each={workspaces()}>
{(directory) => (
<SortableWorkspace
ctx={workspaceSidebarCtx}
directory={directory}
project={p()}
sortNow={sortNow}
mobile={panelProps.mobile}
popover={popover()}
/>
)}
</For>
</SortableProvider>
</div>
<DragOverlay>
<WorkspaceDragOverlay
sidebarProject={sidebarProject}
activeWorkspace={() => store.activeWorkspace}
workspaceLabel={workspaceLabel}
/>
</DragOverlay>
</DragDropProvider>
</div> </div>
</> </>
</Show> }
</div> >
</> <>
)} <div class="shrink-0 py-4">
<Button
size="large"
icon="plus-small"
class="w-full"
onClick={() => {
const item = project()
if (!item) return
createWorkspace(item)
}}
>
{language.t("workspace.new")}
</Button>
</div>
<div class="relative flex-1 min-h-0">
<DragDropProvider
onDragStart={handleWorkspaceDragStart}
onDragEnd={handleWorkspaceDragEnd}
onDragOver={handleWorkspaceDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragXAxis />
<div
ref={(el) => {
if (!panelProps.mobile) scrollContainerRef = el
}}
class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
>
<SortableProvider ids={workspaces()}>
<For each={workspaces()}>
{(directory) => (
<SortableWorkspace
ctx={workspaceSidebarCtx}
directory={directory}
project={project()!}
sortNow={sortNow}
mobile={panelProps.mobile}
popover={popover()}
/>
)}
</For>
</SortableProvider>
</div>
<DragOverlay>
<WorkspaceDragOverlay
sidebarProject={sidebarProject}
activeWorkspace={() => store.activeWorkspace}
workspaceLabel={workspaceLabel}
/>
</DragOverlay>
</DragDropProvider>
</div>
</>
</Show>
</div>
</>
</Show> </Show>
<div <div
@@ -2194,10 +2261,10 @@ export default function Layout(props: ParentProps) {
onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")} onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
renderPanel={() => renderPanel={() =>
mobile ? ( mobile ? (
<SidebarPanel project={currentProject()} mobile /> <SidebarPanel project={currentProject} mobile />
) : ( ) : (
<Show when={currentProject()}> <Show when={currentProject()}>
<SidebarPanel project={currentProject()} merged /> <SidebarPanel project={currentProject} merged />
</Show> </Show>
) )
} }
@@ -2325,8 +2392,8 @@ export default function Layout(props: ParentProps) {
arm() arm()
}} }}
> >
<Show when={state.peek}> <Show when={peekProject()}>
<SidebarPanel project={state.peek} merged={false} /> <SidebarPanel project={peekProject} merged={false} />
</Show> </Show>
</div> </div>