summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/pages/layout/sidebar-items.tsx
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-12 09:49:14 -0600
committerGitHub <[email protected]>2026-02-12 09:49:14 -0600
commitff4414bb152acfddb5c0eb073c38bedc1df4ae14 (patch)
tree78381c67d21ef6f089647f6b19e7aa2976840dbc /packages/app/src/pages/layout/sidebar-items.tsx
parent56ad2db02055955f926fda0e4a89055b22ead6f9 (diff)
downloadopencode-ff4414bb152acfddb5c0eb073c38bedc1df4ae14.tar.gz
opencode-ff4414bb152acfddb5c0eb073c38bedc1df4ae14.zip
chore: refactor packages/app files (#13236)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Frank <[email protected]>
Diffstat (limited to 'packages/app/src/pages/layout/sidebar-items.tsx')
-rw-r--r--packages/app/src/pages/layout/sidebar-items.tsx238
1 files changed, 152 insertions, 86 deletions
diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx
index 678bfa0d8..d55090370 100644
--- a/packages/app/src/pages/layout/sidebar-items.tsx
+++ b/packages/app/src/pages/layout/sidebar-items.tsx
@@ -13,7 +13,7 @@ import { MessageNav } from "@opencode-ai/ui/message-nav"
import { Spinner } from "@opencode-ai/ui/spinner"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { getFilename } from "@opencode-ai/util/path"
-import { type Message, type Session, type TextPart } from "@opencode-ai/sdk/v2/client"
+import { type Message, type Session, type TextPart, type UserMessage } from "@opencode-ai/sdk/v2/client"
import { For, Match, Show, Switch, createMemo, onCleanup, type Accessor, type JSX } from "solid-js"
import { agentColor } from "@/utils/agent"
@@ -70,6 +70,116 @@ export type SessionItemProps = {
archiveSession: (session: Session) => Promise<void>
}
+const SessionRow = (props: {
+ session: Session
+ slug: string
+ mobile?: boolean
+ dense?: boolean
+ tint: Accessor<string | undefined>
+ isWorking: Accessor<boolean>
+ hasPermissions: Accessor<boolean>
+ hasError: Accessor<boolean>
+ unseenCount: Accessor<number>
+ setHoverSession: (id: string | undefined) => void
+ clearHoverProjectSoon: () => void
+ sidebarOpened: Accessor<boolean>
+ prefetchSession: (session: Session, priority?: "high" | "low") => void
+ scheduleHoverPrefetch: () => void
+ cancelHoverPrefetch: () => void
+}): JSX.Element => (
+ <A
+ href={`/${props.slug}/session/${props.session.id}`}
+ class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
+ onPointerEnter={props.scheduleHoverPrefetch}
+ onPointerLeave={props.cancelHoverPrefetch}
+ onMouseEnter={props.scheduleHoverPrefetch}
+ onMouseLeave={props.cancelHoverPrefetch}
+ onFocus={() => props.prefetchSession(props.session, "high")}
+ onClick={() => {
+ props.setHoverSession(undefined)
+ if (props.sidebarOpened()) return
+ props.clearHoverProjectSoon()
+ }}
+ >
+ <div class="flex items-center gap-1 w-full">
+ <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 grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
+ {props.session.title}
+ </span>
+ <Show when={props.session.summary}>
+ {(summary) => (
+ <div class="group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
+ <DiffChanges changes={summary()} />
+ </div>
+ )}
+ </Show>
+ </div>
+ </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 => (
+ <HoverCard
+ openDelay={1000}
+ closeDelay={props.sidebarHovering() ? 600 : 0}
+ placement="right-start"
+ gutter={16}
+ shift={-2}
+ trigger={props.trigger}
+ mount={!props.mobile ? props.nav() : undefined}
+ open={props.hoverSession() === props.session.id}
+ onOpenChange={(open) => props.setHoverSession(open ? props.session.id : undefined)}
+ >
+ <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 max-h-72 h-full">
+ <MessageNav
+ messages={props.hoverMessages() ?? []}
+ current={undefined}
+ getLabel={props.messageLabel}
+ onMessageSelect={props.onMessageSelect}
+ size="normal"
+ class="w-60"
+ />
+ </div>
+ </Show>
+ </HoverCard>
+)
+
export const SessionItem = (props: SessionItemProps): JSX.Element => {
const params = useParams()
const navigate = useNavigate()
@@ -113,7 +223,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
})
const hoverMessages = createMemo(() =>
- sessionStore.message[props.session.id]?.filter((message) => message.role === "user"),
+ sessionStore.message[props.session.id]?.filter((message): message is UserMessage => message.role === "user"),
)
const hoverReady = createMemo(() => sessionStore.message[props.session.id] !== undefined)
const hoverAllowed = createMemo(() => !props.mobile && props.sidebarExpanded())
@@ -141,54 +251,24 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
const text = parts.find((part): part is TextPart => part?.type === "text" && !part.synthetic && !part.ignored)
return text?.text
}
-
const item = (
- <A
- href={`/${props.slug}/session/${props.session.id}`}
- class={`flex items-center justify-between gap-3 min-w-0 text-left w-full focus:outline-none transition-[padding] ${props.mobile ? "pr-7" : ""} group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7 ${props.dense ? "py-0.5" : "py-1"}`}
- onPointerEnter={scheduleHoverPrefetch}
- onPointerLeave={cancelHoverPrefetch}
- onMouseEnter={scheduleHoverPrefetch}
- onMouseLeave={cancelHoverPrefetch}
- onFocus={() => props.prefetchSession(props.session, "high")}
- onClick={() => {
- props.setHoverSession(undefined)
- if (layout.sidebar.opened()) return
- props.clearHoverProjectSoon()
- }}
- >
- <div class="flex items-center gap-1 w-full">
- <div
- class="shrink-0 size-6 flex items-center justify-center"
- style={{ color: tint() ?? "var(--icon-interactive-base)" }}
- >
- <Switch fallback={<Icon name="dash" size="small" class="text-icon-weak" />}>
- <Match when={isWorking()}>
- <Spinner class="size-[15px]" />
- </Match>
- <Match when={hasPermissions()}>
- <div class="size-1.5 rounded-full bg-surface-warning-strong" />
- </Match>
- <Match when={hasError()}>
- <div class="size-1.5 rounded-full bg-text-diff-delete-base" />
- </Match>
- <Match when={unseenCount() > 0}>
- <div class="size-1.5 rounded-full bg-text-interactive-base" />
- </Match>
- </Switch>
- </div>
- <span class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate">
- {props.session.title}
- </span>
- <Show when={props.session.summary}>
- {(summary) => (
- <div class="group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
- <DiffChanges changes={summary()} />
- </div>
- )}
- </Show>
- </div>
- </A>
+ <SessionRow
+ session={props.session}
+ slug={props.slug}
+ mobile={props.mobile}
+ dense={props.dense}
+ tint={tint}
+ isWorking={isWorking}
+ hasPermissions={hasPermissions}
+ hasError={hasError}
+ unseenCount={unseenCount}
+ setHoverSession={props.setHoverSession}
+ clearHoverProjectSoon={props.clearHoverProjectSoon}
+ sidebarOpened={layout.sidebar.opened}
+ prefetchSession={props.prefetchSession}
+ scheduleHoverPrefetch={scheduleHoverPrefetch}
+ cancelHoverPrefetch={cancelHoverPrefetch}
+ />
)
return (
@@ -205,44 +285,30 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
</Tooltip>
}
>
- <HoverCard
- openDelay={1000}
- closeDelay={props.sidebarHovering() ? 600 : 0}
- placement="right-start"
- gutter={16}
- shift={-2}
+ <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)
+ navigate(`${props.slug}/session/${props.session.id}`)
+ return
+ }
+ window.history.replaceState(null, "", `#message-${message.id}`)
+ window.dispatchEvent(new HashChangeEvent("hashchange"))
+ }}
trigger={item}
- mount={!props.mobile ? props.nav() : undefined}
- open={props.hoverSession() === props.session.id}
- onOpenChange={(open) => props.setHoverSession(open ? props.session.id : undefined)}
- >
- <Show
- when={hoverReady()}
- fallback={<div class="text-12-regular text-text-weak">{language.t("session.messages.loading")}</div>}
- >
- <div class="overflow-y-auto max-h-72 h-full">
- <MessageNav
- messages={hoverMessages() ?? []}
- current={undefined}
- getLabel={messageLabel}
- onMessageSelect={(message) => {
- if (!isActive()) {
- layout.pendingMessage.set(
- `${base64Encode(props.session.directory)}/${props.session.id}`,
- message.id,
- )
- navigate(`${props.slug}/session/${props.session.id}`)
- return
- }
- window.history.replaceState(null, "", `#message-${message.id}`)
- window.dispatchEvent(new HashChangeEvent("hashchange"))
- }}
- size="normal"
- class="w-60"
- />
- </div>
- </Show>
- </HoverCard>
+ />
</Show>
<div
class={`absolute ${props.dense ? "top-0.5 right-0.5" : "top-1 right-1"} flex items-center gap-0.5 transition-opacity`}