summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/pages/layout
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-04-07 11:06:23 -0500
committerGitHub <[email protected]>2026-04-07 11:06:23 -0500
commitec8b9810b4231cd6a5c69ccd930b6c50999fc997 (patch)
tree562313d6dd3eda9891f3a4a3a2ef6ce3d36acd05 /packages/app/src/pages/layout
parent65318a80f7a3320ba77b749241f8de997dc65c82 (diff)
downloadopencode-ec8b9810b4231cd6a5c69ccd930b6c50999fc997.tar.gz
opencode-ec8b9810b4231cd6a5c69ccd930b6c50999fc997.zip
feat(app): better subagent experience (#20708)
Diffstat (limited to 'packages/app/src/pages/layout')
-rw-r--r--packages/app/src/pages/layout/helpers.test.ts14
-rw-r--r--packages/app/src/pages/layout/helpers.ts21
-rw-r--r--packages/app/src/pages/layout/sidebar-items.tsx296
-rw-r--r--packages/app/src/pages/layout/sidebar-project.tsx27
-rw-r--r--packages/app/src/pages/layout/sidebar-workspace.tsx26
5 files changed, 123 insertions, 261 deletions
diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts
index 1fe52d47a..988332ab7 100644
--- a/packages/app/src/pages/layout/helpers.test.ts
+++ b/packages/app/src/pages/layout/helpers.test.ts
@@ -8,6 +8,7 @@ import {
} from "./deep-links"
import { type Session } from "@opencode-ai/sdk/v2/client"
import {
+ childSessionOnPath,
displayName,
effectiveWorkspaceOrder,
errorMessage,
@@ -198,6 +199,19 @@ describe("layout workspace helpers", () => {
expect(result?.id).toBe("root")
})
+ test("finds the direct child on the active session path", () => {
+ const list = [
+ session({ id: "root", directory: "/workspace" }),
+ session({ id: "child", directory: "/workspace", parentID: "root" }),
+ session({ id: "leaf", directory: "/workspace", parentID: "child" }),
+ ]
+
+ expect(childSessionOnPath(list, "root", "leaf")?.id).toBe("child")
+ expect(childSessionOnPath(list, "child", "leaf")?.id).toBe("leaf")
+ expect(childSessionOnPath(list, "root", "root")).toBeUndefined()
+ expect(childSessionOnPath(list, "root", "other")).toBeUndefined()
+ })
+
test("formats fallback project display name", () => {
expect(displayName({ worktree: "/tmp/app" })).toBe("app")
expect(displayName({ worktree: "/tmp/app", name: "My App" })).toBe("My App")
diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts
index 226098c1c..48158debb 100644
--- a/packages/app/src/pages/layout/helpers.ts
+++ b/packages/app/src/pages/layout/helpers.ts
@@ -46,18 +46,17 @@ export function hasProjectPermissions<T>(
return Object.values(request ?? {}).some((list) => list?.some(include))
}
-export const childMapByParent = (sessions: Session[] | undefined) => {
- const map = new Map<string, string[]>()
- for (const session of sessions ?? []) {
- if (!session.parentID) continue
- const existing = map.get(session.parentID)
- if (existing) {
- existing.push(session.id)
- continue
- }
- map.set(session.parentID, [session.id])
+export const childSessionOnPath = (sessions: Session[] | undefined, rootID: string, activeID?: string) => {
+ if (!activeID || activeID === rootID) return
+ const map = new Map((sessions ?? []).map((session) => [session.id, session]))
+ let id = activeID
+
+ while (id) {
+ const session = map.get(id)
+ if (!session?.parentID) return
+ if (session.parentID === rootID) return session
+ id = session.parentID
}
- return map
}
export const displayName = (project: { name?: string; worktree: string }) =>
diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx
index 058bb5a0d..e56accfc8 100644
--- a/packages/app/src/pages/layout/sidebar-items.tsx
+++ b/packages/app/src/pages/layout/sidebar-items.tsx
@@ -1,15 +1,12 @@
-import type { Message, Session, TextPart, UserMessage } from "@opencode-ai/sdk/v2/client"
+import type { Session } from "@opencode-ai/sdk/v2/client"
import { Avatar } from "@opencode-ai/ui/avatar"
-import { HoverCard } from "@opencode-ai/ui/hover-card"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
-import { MessageNav } from "@opencode-ai/ui/message-nav"
import { Spinner } from "@opencode-ai/ui/spinner"
import { Tooltip } from "@opencode-ai/ui/tooltip"
-import { base64Encode } from "@opencode-ai/util/encode"
import { getFilename } from "@opencode-ai/util/path"
-import { A, useNavigate, useParams } from "@solidjs/router"
-import { type Accessor, createMemo, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js"
+import { A, useParams } from "@solidjs/router"
+import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
@@ -18,7 +15,7 @@ import { usePermission } from "@/context/permission"
import { messageAgentColor } from "@/utils/agent"
import { sessionTitle } from "@/utils/session-title"
import { sessionPermissionRequest } from "../session/composer/session-request-tree"
-import { hasProjectPermissions } from "./helpers"
+import { childSessionOnPath, hasProjectPermissions } from "./helpers"
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
@@ -39,6 +36,7 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
)
const notify = createMemo(() => props.notify && (hasPermissions() || unseenCount() > 0))
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
+
return (
<div class={`relative size-8 shrink-0 rounded ${props.class ?? ""}`}>
<div class="size-full rounded overflow-clip">
@@ -73,13 +71,10 @@ export type SessionItemProps = {
slug: string
mobile?: boolean
dense?: boolean
- popover?: boolean
- children: Map<string, string[]>
+ showTooltip?: boolean
+ showChild?: boolean
+ level?: number
sidebarExpanded: Accessor<boolean>
- sidebarHovering: Accessor<boolean>
- nav: Accessor<HTMLElement | undefined>
- hoverSession: Accessor<string | undefined>
- setHoverSession: (id: string | undefined) => void
clearHoverProjectSoon: () => void
prefetchSession: (session: Session, priority?: "high" | "low") => void
archiveSession: (session: Session) => Promise<void>
@@ -95,116 +90,52 @@ const SessionRow = (props: {
hasPermissions: Accessor<boolean>
hasError: Accessor<boolean>
unseenCount: Accessor<number>
- setHoverSession: (id: string | undefined) => void
clearHoverProjectSoon: () => void
sidebarOpened: Accessor<boolean>
- warmHover: () => void
warmPress: () => void
warmFocus: () => void
- cancelHoverPrefetch: () => void
-}) => {
+}): JSX.Element => {
const title = () => sessionTitle(props.session.title)
return (
<A
href={`/${props.slug}/session/${props.session.id}`}
- class={`flex items-center gap-1 min-w-0 w-full text-left focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
+ class={`flex items-center gap-2 min-w-0 w-full text-left focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
onPointerDown={props.warmPress}
- onPointerEnter={props.warmHover}
- onPointerLeave={props.cancelHoverPrefetch}
onFocus={props.warmFocus}
onClick={() => {
- props.setHoverSession(undefined)
if (props.sidebarOpened()) return
props.clearHoverProjectSoon()
}}
>
- <div
- class="shrink-0 size-6 flex items-center justify-center"
- style={{ color: props.tint() ?? "var(--icon-interactive-base)" }}
- >
- <Switch fallback={<Icon name="dash" size="small" class="text-icon-weak" />}>
- <Match when={props.isWorking()}>
- <Spinner class="size-[15px]" />
- </Match>
- <Match when={props.hasPermissions()}>
- <div class="size-1.5 rounded-full bg-surface-warning-strong" />
- </Match>
- <Match when={props.hasError()}>
- <div class="size-1.5 rounded-full bg-text-diff-delete-base" />
- </Match>
- <Match when={props.unseenCount() > 0}>
- <div class="size-1.5 rounded-full bg-text-interactive-base" />
- </Match>
- </Switch>
- </div>
- <span class="text-14-regular text-text-strong min-w-0 flex-1 truncate">{title()}</span>
- </A>
- )
-}
-
-const SessionHoverPreview = (props: {
- mobile?: boolean
- nav: Accessor<HTMLElement | undefined>
- hoverSession: Accessor<string | undefined>
- session: Session
- sidebarHovering: Accessor<boolean>
- hoverReady: Accessor<boolean>
- hoverMessages: Accessor<UserMessage[] | undefined>
- language: ReturnType<typeof useLanguage>
- isActive: Accessor<boolean>
- slug: string
- setHoverSession: (id: string | undefined) => void
- messageLabel: (message: Message) => string | undefined
- onMessageSelect: (message: Message) => void
- trigger: JSX.Element
-}): JSX.Element => {
- let ref: HTMLDivElement | undefined
-
- return (
- <HoverCard
- openDelay={1000}
- closeDelay={props.sidebarHovering() ? 600 : 0}
- placement="right-start"
- gutter={16}
- shift={-2}
- trigger={
- <div ref={ref} class="min-w-0 w-full">
- {props.trigger}
- </div>
- }
- open={props.hoverSession() === props.session.id}
- onOpenChange={(open) => {
- if (!open) {
- props.setHoverSession(undefined)
- return
- }
- if (!ref?.matches(":hover")) return
- props.setHoverSession(props.session.id)
- }}
- >
- <Show
- when={props.hoverReady()}
- fallback={<div class="text-12-regular text-text-weak">{props.language.t("session.messages.loading")}</div>}
- >
- <div class="overflow-y-auto overflow-x-hidden max-h-72 h-full">
- <MessageNav
- messages={props.hoverMessages() ?? []}
- current={undefined}
- getLabel={props.messageLabel}
- onMessageSelect={props.onMessageSelect}
- size="normal"
- class="w-60"
- />
+ <Show when={props.isWorking() || props.hasPermissions() || props.hasError() || props.unseenCount() > 0}>
+ <div
+ class="shrink-0 size-6 flex items-center justify-center"
+ style={{ color: props.tint() ?? "var(--icon-interactive-base)" }}
+ >
+ <Switch>
+ <Match when={props.isWorking()}>
+ <Spinner class="size-[15px]" />
+ </Match>
+ <Match when={props.hasPermissions()}>
+ <div class="size-1.5 rounded-full bg-surface-warning-strong" />
+ </Match>
+ <Match when={props.hasError()}>
+ <div class="size-1.5 rounded-full bg-text-diff-delete-base" />
+ </Match>
+ <Match when={props.unseenCount() > 0}>
+ <div class="size-1.5 rounded-full bg-text-interactive-base" />
+ </Match>
+ </Switch>
</div>
</Show>
- </HoverCard>
+ <span class="text-14-regular text-text-strong min-w-0 flex-1 truncate">{title()}</span>
+ </A>
)
}
export const SessionItem = (props: SessionItemProps): JSX.Element => {
const params = useParams()
- const navigate = useNavigate()
const layout = useLayout()
const language = useLanguage()
const notification = useNotification()
@@ -234,18 +165,13 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
)
})
- const tint = createMemo(() => {
- return messageAgentColor(sessionStore.message[props.session.id], sessionStore.agent)
+ const tint = createMemo(() => messageAgentColor(sessionStore.message[props.session.id], sessionStore.agent))
+ const tooltip = createMemo(() => props.showTooltip ?? (props.mobile || !props.sidebarExpanded()))
+ const currentChild = createMemo(() => {
+ if (!props.showChild) return
+ return childSessionOnPath(sessionStore.session, props.session.id, params.id)
})
- const hoverMessages = createMemo(() =>
- sessionStore.message[props.session.id]?.filter((message): message is UserMessage => message.role === "user"),
- )
- const hoverReady = createMemo(() => hoverMessages() !== undefined)
- const hoverAllowed = createMemo(() => !props.mobile && props.sidebarExpanded())
- const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed())
- const isActive = createMemo(() => props.session.id === params.id)
-
const warm = (span: number, priority: "high" | "low") => {
const nav = props.navList?.()
const list = nav?.some((item) => item.id === props.session.id && item.directory === props.session.directory)
@@ -266,30 +192,6 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
}
}
- const hoverPrefetch = {
- current: undefined as ReturnType<typeof setTimeout> | undefined,
- }
- const cancelHoverPrefetch = () => {
- if (hoverPrefetch.current === undefined) return
- clearTimeout(hoverPrefetch.current)
- hoverPrefetch.current = undefined
- }
- const scheduleHoverPrefetch = () => {
- warm(1, "high")
- if (hoverPrefetch.current !== undefined) return
- hoverPrefetch.current = setTimeout(() => {
- hoverPrefetch.current = undefined
- warm(2, "low")
- }, 80)
- }
-
- onCleanup(cancelHoverPrefetch)
-
- const messageLabel = (message: Message) => {
- const parts = sessionStore.part[message.id] ?? []
- const text = parts.find((part): part is TextPart => part?.type === "text" && !part.synthetic && !part.ignored)
- return text?.text
- }
const item = (
<SessionRow
session={props.session}
@@ -301,86 +203,74 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
hasPermissions={hasPermissions}
hasError={hasError}
unseenCount={unseenCount}
- setHoverSession={props.setHoverSession}
clearHoverProjectSoon={props.clearHoverProjectSoon}
sidebarOpened={layout.sidebar.opened}
- warmHover={scheduleHoverPrefetch}
warmPress={() => warm(2, "high")}
warmFocus={() => warm(2, "high")}
- cancelHoverPrefetch={cancelHoverPrefetch}
/>
)
return (
- <div
- data-session-id={props.session.id}
- class="group/session relative w-full min-w-0 rounded-md cursor-default pl-2 pr-3 transition-colors
- hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
- >
- <div class="flex min-w-0 items-center gap-1">
- <div class="min-w-0 flex-1">
- <Show
- when={hoverEnabled()}
- fallback={
- <Tooltip
- placement={props.mobile ? "bottom" : "right"}
- value={sessionTitle(props.session.title)}
- gutter={10}
- class="min-w-0 w-full"
- >
- {item}
- </Tooltip>
- }
- >
- <SessionHoverPreview
- mobile={props.mobile}
- nav={props.nav}
- hoverSession={props.hoverSession}
- session={props.session}
- sidebarHovering={props.sidebarHovering}
- hoverReady={hoverReady}
- hoverMessages={hoverMessages}
- language={language}
- isActive={isActive}
- slug={props.slug}
- setHoverSession={props.setHoverSession}
- messageLabel={messageLabel}
- onMessageSelect={(message) => {
- if (!isActive())
- layout.pendingMessage.set(`${base64Encode(props.session.directory)}/${props.session.id}`, message.id)
+ <>
+ <div
+ data-session-id={props.session.id}
+ class="group/session relative w-full min-w-0 rounded-md cursor-default pr-3 transition-colors hover:bg-surface-raised-base-hover [&:has(:focus-visible)]:bg-surface-raised-base-hover has-[[data-expanded]]:bg-surface-raised-base-hover has-[.active]:bg-surface-base-active"
+ style={{ "padding-left": `${8 + (props.level ?? 0) * 16}px` }}
+ >
+ <div class="flex min-w-0 items-center gap-1">
+ <div class="min-w-0 flex-1">
+ <Show
+ when={!tooltip()}
+ fallback={
+ <Tooltip
+ placement={props.mobile ? "bottom" : "right"}
+ value={sessionTitle(props.session.title)}
+ gutter={10}
+ class="min-w-0 w-full"
+ >
+ {item}
+ </Tooltip>
+ }
+ >
+ {item}
+ </Show>
+ </div>
- navigate(`${props.slug}/session/${props.session.id}#message-${message.id}`)
+ <Show when={!props.level}>
+ <div
+ class="shrink-0 overflow-hidden transition-[width,opacity]"
+ classList={{
+ "w-6 opacity-100 pointer-events-auto": !!props.mobile,
+ "w-0 opacity-0 pointer-events-none": !props.mobile,
+ "group-hover/session:w-6 group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true,
+ "group-focus-within/session:w-6 group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true,
}}
- trigger={item}
- />
+ >
+ <Tooltip value={language.t("common.archive")} placement="top">
+ <IconButton
+ icon="archive"
+ variant="ghost"
+ class="size-6 rounded-md"
+ aria-label={language.t("common.archive")}
+ onClick={(event) => {
+ event.preventDefault()
+ event.stopPropagation()
+ void props.archiveSession(props.session)
+ }}
+ />
+ </Tooltip>
+ </div>
</Show>
</div>
-
- <div
- class="shrink-0 overflow-hidden transition-[width,opacity]"
- classList={{
- "w-6 opacity-100 pointer-events-auto": !!props.mobile,
- "w-0 opacity-0 pointer-events-none": !props.mobile,
- "group-hover/session:w-6 group-hover/session:opacity-100 group-hover/session:pointer-events-auto": true,
- "group-focus-within/session:w-6 group-focus-within/session:opacity-100 group-focus-within/session:pointer-events-auto": true,
- }}
- >
- <Tooltip value={language.t("common.archive")} placement="top">
- <IconButton
- icon="archive"
- variant="ghost"
- class="size-6 rounded-md"
- aria-label={language.t("common.archive")}
- onClick={(event) => {
- event.preventDefault()
- event.stopPropagation()
- void props.archiveSession(props.session)
- }}
- />
- </Tooltip>
- </div>
</div>
- </div>
+ <Show when={currentChild()}>
+ {(child) => (
+ <div class="w-full">
+ <SessionItem {...props} session={child()} level={(props.level ?? 0) + 1} />
+ </div>
+ )}
+ </Show>
+ </>
)
}
@@ -390,7 +280,6 @@ export const NewSessionItem = (props: {
dense?: boolean
sidebarExpanded: Accessor<boolean>
clearHoverProjectSoon: () => void
- setHoverSession: (id: string | undefined) => void
}): JSX.Element => {
const layout = useLayout()
const language = useLanguage()
@@ -400,9 +289,8 @@ export const NewSessionItem = (props: {
<A
href={`/${props.slug}/session`}
end
- class={`flex items-center gap-1 min-w-0 w-full text-left focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
+ class={`flex items-center gap-2 min-w-0 w-full text-left focus:outline-none ${props.dense ? "py-0.5" : "py-1"}`}
onClick={() => {
- props.setHoverSession(undefined)
if (layout.sidebar.opened()) return
props.clearHoverProjectSoon()
}}
diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx
index aff0645dd..7c9ae1aaf 100644
--- a/packages/app/src/pages/layout/sidebar-project.tsx
+++ b/packages/app/src/pages/layout/sidebar-project.tsx
@@ -1,4 +1,4 @@
-import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js"
+import { createMemo, For, Show, type Accessor, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { base64Encode } from "@opencode-ai/util/encode"
import { Button } from "@opencode-ai/ui/button"
@@ -11,7 +11,7 @@ import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { useNotification } from "@/context/notification"
import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items"
-import { childMapByParent, displayName, sortedRootSessions } from "./helpers"
+import { displayName, sortedRootSessions } from "./helpers"
export type ProjectSidebarContext = {
currentDir: Accessor<string>
@@ -19,7 +19,6 @@ export type ProjectSidebarContext = {
sidebarOpened: Accessor<boolean>
sidebarHovering: Accessor<boolean>
hoverProject: Accessor<string | undefined>
- nav: Accessor<HTMLElement | undefined>
onProjectMouseEnter: (worktree: string, event: MouseEvent) => void
onProjectMouseLeave: (worktree: string) => void
onProjectFocus: (worktree: string) => void
@@ -32,8 +31,7 @@ export type ProjectSidebarContext = {
workspacesEnabled: (project: LocalProject) => boolean
workspaceIds: (project: LocalProject) => string[]
workspaceLabel: (directory: string, branch?: string, projectId?: string) => string
- sessionProps: Omit<SessionItemProps, "session" | "list" | "slug" | "children" | "mobile" | "dense" | "popover">
- setHoverSession: (id: string | undefined) => void
+ sessionProps: Omit<SessionItemProps, "session" | "list" | "slug" | "mobile" | "dense">
}
export const ProjectDragOverlay = (props: {
@@ -55,7 +53,6 @@ export const ProjectDragOverlay = (props: {
const ProjectTile = (props: {
project: LocalProject
mobile?: boolean
- nav: Accessor<HTMLElement | undefined>
sidebarHovering: Accessor<boolean>
selected: Accessor<boolean>
active: Accessor<boolean>
@@ -195,9 +192,7 @@ const ProjectPreviewPanel = (props: {
workspaces: Accessor<string[]>
label: (directory: string) => string
projectSessions: Accessor<ReturnType<typeof sortedRootSessions>>
- projectChildren: Accessor<Map<string, string[]>>
workspaceSessions: (directory: string) => ReturnType<typeof sortedRootSessions>
- workspaceChildren: (directory: string) => Map<string, string[]>
ctx: ProjectSidebarContext
language: ReturnType<typeof useLanguage>
}): JSX.Element => (
@@ -218,9 +213,8 @@ const ProjectPreviewPanel = (props: {
list={props.projectSessions()}
slug={base64Encode(props.project.worktree)}
dense
+ showTooltip
mobile={props.mobile}
- popover={false}
- children={props.projectChildren()}
/>
)}
</For>
@@ -229,7 +223,6 @@ const ProjectPreviewPanel = (props: {
<For each={props.workspaces()}>
{(directory) => {
const sessions = createMemo(() => props.workspaceSessions(directory))
- const children = createMemo(() => props.workspaceChildren(directory))
return (
<div class="flex flex-col gap-1">
<div class="px-2 py-0.5 flex items-center gap-1 min-w-0">
@@ -246,9 +239,8 @@ const ProjectPreviewPanel = (props: {
list={sessions()}
slug={base64Encode(directory)}
dense
+ showTooltip
mobile={props.mobile}
- popover={false}
- children={children()}
/>
)}
</For>
@@ -310,20 +302,14 @@ export const SortableProject = (props: {
const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0])
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()))
- const projectChildren = createMemo(() => childMapByParent(projectStore().session))
const workspaceSessions = (directory: string) => {
const [data] = globalSync.child(directory, { bootstrap: false })
return sortedRootSessions(data, props.sortNow())
}
- const workspaceChildren = (directory: string) => {
- const [data] = globalSync.child(directory, { bootstrap: false })
- return childMapByParent(data.session)
- }
const tile = () => (
<ProjectTile
project={props.project}
mobile={props.mobile}
- nav={props.ctx.nav}
sidebarHovering={props.ctx.sidebarHovering}
selected={selected}
active={active}
@@ -360,7 +346,6 @@ export const SortableProject = (props: {
if (state.menu) return
if (value && state.suppressHover) return
props.ctx.onHoverOpenChanged(props.project.worktree, value)
- if (value) props.ctx.setHoverSession(undefined)
}}
>
<ProjectPreviewPanel
@@ -371,9 +356,7 @@ export const SortableProject = (props: {
workspaces={workspaces}
label={label}
projectSessions={projectSessions}
- projectChildren={projectChildren}
workspaceSessions={workspaceSessions}
- workspaceChildren={workspaceChildren}
ctx={props.ctx}
language={language}
/>
diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx
index 3bf00ea42..68e36ff77 100644
--- a/packages/app/src/pages/layout/sidebar-workspace.tsx
+++ b/packages/app/src/pages/layout/sidebar-workspace.tsx
@@ -17,7 +17,7 @@ import { type LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
-import { childMapByParent, sortedRootSessions, workspaceKey } from "./helpers"
+import { sortedRootSessions, workspaceKey } from "./helpers"
type InlineEditorComponent = (props: {
id: string
@@ -35,9 +35,6 @@ export type WorkspaceSidebarContext = {
navList: Accessor<Session[]>
sidebarExpanded: Accessor<boolean>
sidebarHovering: Accessor<boolean>
- nav: Accessor<HTMLElement | undefined>
- hoverSession: Accessor<string | undefined>
- setHoverSession: (id: string | undefined) => void
clearHoverProjectSoon: () => void
prefetchSession: (session: Session, priority?: "high" | "low") => void
archiveSession: (session: Session) => Promise<void>
@@ -152,7 +149,6 @@ const WorkspaceActions = (props: {
showResetWorkspaceDialog: WorkspaceSidebarContext["showResetWorkspaceDialog"]
showDeleteWorkspaceDialog: WorkspaceSidebarContext["showDeleteWorkspaceDialog"]
root: string
- setHoverSession: WorkspaceSidebarContext["setHoverSession"]
clearHoverProjectSoon: WorkspaceSidebarContext["clearHoverProjectSoon"]
navigateToNewSession: () => void
}): JSX.Element => (
@@ -226,7 +222,6 @@ const WorkspaceActions = (props: {
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
- props.setHoverSession(undefined)
props.clearHoverProjectSoon()
props.navigateToNewSession()
}}
@@ -239,12 +234,10 @@ const WorkspaceActions = (props: {
const WorkspaceSessionList = (props: {
slug: Accessor<string>
mobile?: boolean
- popover?: boolean
ctx: WorkspaceSidebarContext
showNew: Accessor<boolean>
loading: Accessor<boolean>
sessions: Accessor<Session[]>
- children: Accessor<Map<string, string[]>>
hasMore: Accessor<boolean>
loadMore: () => Promise<void>
language: ReturnType<typeof useLanguage>
@@ -256,7 +249,6 @@ const WorkspaceSessionList = (props: {
mobile={props.mobile}
sidebarExpanded={props.ctx.sidebarExpanded}
clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
- setHoverSession={props.ctx.setHoverSession}
/>
</Show>
<Show when={props.loading()}>
@@ -270,13 +262,8 @@ const WorkspaceSessionList = (props: {
navList={props.ctx.navList}
slug={props.slug()}
mobile={props.mobile}
- popover={props.popover}
- children={props.children()}
+ showChild
sidebarExpanded={props.ctx.sidebarExpanded}
- sidebarHovering={props.ctx.sidebarHovering}
- nav={props.ctx.nav}
- hoverSession={props.ctx.hoverSession}
- setHoverSession={props.ctx.setHoverSession}
clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
prefetchSession={props.ctx.prefetchSession}
archiveSession={props.ctx.archiveSession}
@@ -307,7 +294,6 @@ export const SortableWorkspace = (props: {
project: LocalProject
sortNow: Accessor<number>
mobile?: boolean
- popover?: boolean
}): JSX.Element => {
const navigate = useNavigate()
const params = useParams()
@@ -321,7 +307,6 @@ export const SortableWorkspace = (props: {
})
const slug = createMemo(() => base64Encode(props.directory))
const sessions = createMemo(() => sortedRootSessions(workspaceStore, props.sortNow()))
- const children = createMemo(() => childMapByParent(workspaceStore.session))
const local = createMemo(() => props.directory === props.project.worktree)
const active = createMemo(() => workspaceKey(props.ctx.currentDir()) === workspaceKey(props.directory))
const workspaceValue = createMemo(() => {
@@ -428,7 +413,6 @@ export const SortableWorkspace = (props: {
showResetWorkspaceDialog={props.ctx.showResetWorkspaceDialog}
showDeleteWorkspaceDialog={props.ctx.showDeleteWorkspaceDialog}
root={props.project.worktree}
- setHoverSession={props.ctx.setHoverSession}
clearHoverProjectSoon={props.ctx.clearHoverProjectSoon}
navigateToNewSession={() => navigate(`/${slug()}/session`)}
/>
@@ -440,12 +424,10 @@ export const SortableWorkspace = (props: {
<WorkspaceSessionList
slug={slug}
mobile={props.mobile}
- popover={props.popover}
ctx={props.ctx}
showNew={showNew}
loading={loading}
sessions={sessions}
- children={children}
hasMore={hasMore}
loadMore={loadMore}
language={language}
@@ -461,7 +443,6 @@ export const LocalWorkspace = (props: {
project: LocalProject
sortNow: Accessor<number>
mobile?: boolean
- popover?: boolean
}): JSX.Element => {
const globalSync = useGlobalSync()
const language = useLanguage()
@@ -471,7 +452,6 @@ export const LocalWorkspace = (props: {
})
const slug = createMemo(() => base64Encode(props.project.worktree))
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 count = createMemo(() => sessions()?.length ?? 0)
const loading = createMemo(() => !booted() && count() === 0)
@@ -489,12 +469,10 @@ export const LocalWorkspace = (props: {
<WorkspaceSessionList
slug={slug}
mobile={props.mobile}
- popover={props.popover}
ctx={props.ctx}
showNew={() => false}
loading={loading}
sessions={sessions}
- children={children}
hasMore={hasMore}
loadMore={loadMore}
language={language}