From ff4414bb152acfddb5c0eb073c38bedc1df4ae14 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 12 Feb 2026 09:49:14 -0600 Subject: chore: refactor packages/app files (#13236) Co-authored-by: opencode-agent[bot] Co-authored-by: Frank --- packages/app/src/pages/layout/inline-editor.tsx | 17 +- packages/app/src/pages/layout/sidebar-items.tsx | 238 +++++++---- packages/app/src/pages/layout/sidebar-project.tsx | 381 +++++++++++------- packages/app/src/pages/layout/sidebar-shell.tsx | 11 +- .../app/src/pages/layout/sidebar-workspace.tsx | 445 +++++++++++++-------- 5 files changed, 676 insertions(+), 416 deletions(-) (limited to 'packages/app/src/pages/layout') diff --git a/packages/app/src/pages/layout/inline-editor.tsx b/packages/app/src/pages/layout/inline-editor.tsx index 0bbfe244c..4189e4a72 100644 --- a/packages/app/src/pages/layout/inline-editor.tsx +++ b/packages/app/src/pages/layout/inline-editor.tsx @@ -1,8 +1,9 @@ import { createStore } from "solid-js/store" -import { Show, type Accessor } from "solid-js" +import { onCleanup, Show, type Accessor } from "solid-js" import { InlineInput } from "@opencode-ai/ui/inline-input" export function createInlineEditorController() { + // This controller intentionally supports one active inline editor at a time. const [editor, setEditor] = createStore({ active: "" as string, value: "", @@ -47,6 +48,13 @@ export function createInlineEditorController() { stopPropagation?: boolean openOnDblClick?: boolean }) => { + let frame: number | undefined + + onCleanup(() => { + if (frame === undefined) return + cancelAnimationFrame(frame) + }) + const isEditing = () => props.editing ?? editorOpen(props.id) const stopEvents = () => props.stopPropagation ?? false const allowDblClick = () => props.openOnDblClick ?? true @@ -78,7 +86,12 @@ export function createInlineEditorController() { > { - requestAnimationFrame(() => el.focus()) + if (frame !== undefined) cancelAnimationFrame(frame) + frame = requestAnimationFrame(() => { + frame = undefined + if (!el.isConnected) return + el.focus() + }) }} value={editorValue()} class={props.class} 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 } +const SessionRow = (props: { + session: Session + slug: string + mobile?: boolean + dense?: boolean + tint: Accessor + isWorking: Accessor + hasPermissions: Accessor + hasError: Accessor + unseenCount: Accessor + setHoverSession: (id: string | undefined) => void + clearHoverProjectSoon: () => void + sidebarOpened: Accessor + prefetchSession: (session: Session, priority?: "high" | "low") => void + scheduleHoverPrefetch: () => void + cancelHoverPrefetch: () => void +}): JSX.Element => ( + props.prefetchSession(props.session, "high")} + onClick={() => { + props.setHoverSession(undefined) + if (props.sidebarOpened()) return + props.clearHoverProjectSoon() + }} + > +
+
+ }> + + + + +
+ + +
+ + 0}> +
+ + +
+ + {props.session.title} + + + {(summary) => ( +
+ +
+ )} +
+
+
+) + +const SessionHoverPreview = (props: { + mobile?: boolean + nav: Accessor + hoverSession: Accessor + session: Session + sidebarHovering: Accessor + hoverReady: Accessor + hoverMessages: Accessor + language: ReturnType + isActive: Accessor + slug: string + setHoverSession: (id: string | undefined) => void + messageLabel: (message: Message) => string | undefined + onMessageSelect: (message: Message) => void + trigger: JSX.Element +}): JSX.Element => ( + props.setHoverSession(open ? props.session.id : undefined)} + > + {props.language.t("session.messages.loading")}
} + > +
+ +
+ + +) + 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 = ( - props.prefetchSession(props.session, "high")} - onClick={() => { - props.setHoverSession(undefined) - if (layout.sidebar.opened()) return - props.clearHoverProjectSoon() - }} - > -
-
- }> - - - - -
- - -
- - 0}> -
- - -
- - {props.session.title} - - - {(summary) => ( -
- -
- )} -
-
-
+ ) return ( @@ -205,44 +285,30 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => { } > - { + 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)} - > - {language.t("session.messages.loading")}
} - > -
- { - 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" - /> -
- - + />
+ sidebarHovering: Accessor + selected: Accessor + active: Accessor + overlay: Accessor + onProjectMouseEnter: (worktree: string, event: MouseEvent) => void + onProjectMouseLeave: (worktree: string) => void + onProjectFocus: (worktree: string) => void + navigateToProject: (directory: string) => void + showEditProjectDialog: (project: LocalProject) => void + toggleProjectWorkspaces: (project: LocalProject) => void + workspacesEnabled: (project: LocalProject) => boolean + closeProject: (directory: string) => void + setMenu: (value: boolean) => void + setOpen: (value: boolean) => void + language: ReturnType +}): JSX.Element => ( + { + props.setMenu(value) + if (value) props.setOpen(false) + }} + > + { + if (!props.overlay()) return + props.onProjectMouseEnter(props.project.worktree, event) + }} + onMouseLeave={() => { + if (!props.overlay()) return + props.onProjectMouseLeave(props.project.worktree) + }} + onFocus={() => { + if (!props.overlay()) return + props.onProjectFocus(props.project.worktree) + }} + onClick={() => props.navigateToProject(props.project.worktree)} + onBlur={() => props.setOpen(false)} + > + + + + + props.showEditProjectDialog(props.project)}> + {props.language.t("common.edit")} + + props.toggleProjectWorkspaces(props.project)} + > + + {props.workspacesEnabled(props.project) + ? props.language.t("sidebar.workspaces.disable") + : props.language.t("sidebar.workspaces.enable")} + + + + props.closeProject(props.project.worktree)} + > + {props.language.t("common.close")} + + + + +) + +const ProjectPreviewPanel = (props: { + project: LocalProject + mobile?: boolean + selected: Accessor + workspaceEnabled: Accessor + workspaces: Accessor + label: (directory: string) => string + projectSessions: Accessor> + projectChildren: Accessor> + workspaceSessions: (directory: string) => ReturnType + workspaceChildren: (directory: string) => Map + setOpen: (value: boolean) => void + ctx: ProjectSidebarContext + language: ReturnType +}): JSX.Element => ( +
+
+
{displayName(props.project)}
+ + { + event.stopPropagation() + props.setOpen(false) + props.ctx.closeProject(props.project.worktree) + }} + /> + +
+
{props.language.t("sidebar.project.recentSessions")}
+
+ + {(session) => ( + + )} + + } + > + + {(directory) => { + const sessions = createMemo(() => props.workspaceSessions(directory)) + const children = createMemo(() => props.workspaceChildren(directory)) + return ( +
+
+
+ +
+ {props.label(directory)} +
+ + {(session) => ( + + )} + +
+ ) + }} +
+
+
+
+ +
+
+) + export const SortableProject = (props: { project: LocalProject mobile?: boolean @@ -105,177 +294,61 @@ export const SortableProject = (props: { const [data] = globalSync.child(directory, { bootstrap: false }) return childMapByParent(data.session) } - - const Trigger = () => ( - { - setMenu(value) - if (value) setOpen(false) - }} - > - { - if (!overlay()) return - props.ctx.onProjectMouseEnter(props.project.worktree, event) - }} - onMouseLeave={() => { - if (!overlay()) return - props.ctx.onProjectMouseLeave(props.project.worktree) - }} - onFocus={() => { - if (!overlay()) return - props.ctx.onProjectFocus(props.project.worktree) - }} - onClick={() => props.ctx.navigateToProject(props.project.worktree)} - onBlur={() => setOpen(false)} - > - - - - - props.ctx.showEditProjectDialog(props.project)}> - {language.t("common.edit")} - - props.ctx.toggleProjectWorkspaces(props.project)} - > - - {props.ctx.workspacesEnabled(props.project) - ? language.t("sidebar.workspaces.disable") - : language.t("sidebar.workspaces.enable")} - - - - props.ctx.closeProject(props.project.worktree)} - > - {language.t("common.close")} - - - - + const trigger = ( + ) return ( // @ts-ignore
- }> + } + trigger={trigger} onOpenChange={(value) => { if (menu()) return setOpen(value) if (value) props.ctx.setHoverSession(undefined) }} > -
-
-
{displayName(props.project)}
- - { - event.stopPropagation() - setOpen(false) - props.ctx.closeProject(props.project.worktree) - }} - /> - -
-
{language.t("sidebar.project.recentSessions")}
-
- - {(session) => ( - - )} - - } - > - - {(directory) => { - const sessions = createMemo(() => workspaceSessions(directory)) - const children = createMemo(() => workspaceChildren(directory)) - return ( -
-
-
- -
- {label(directory)} -
- - {(session) => ( - - )} - -
- ) - }} -
-
-
-
- -
-
+
diff --git a/packages/app/src/pages/layout/sidebar-shell.tsx b/packages/app/src/pages/layout/sidebar-shell.tsx index ce96a09d1..23abdf157 100644 --- a/packages/app/src/pages/layout/sidebar-shell.tsx +++ b/packages/app/src/pages/layout/sidebar-shell.tsx @@ -34,6 +34,7 @@ export const SidebarContent = (props: { renderPanel: () => JSX.Element }): JSX.Element => { const expanded = createMemo(() => sidebarExpanded(props.mobile, props.opened())) + const placement = () => (props.mobile ? "bottom" : "right") return (
@@ -55,7 +56,7 @@ export const SidebarContent = (props: { {(project) => props.renderProject(project)} {props.openProjectLabel} @@ -78,11 +79,7 @@ export const SidebarContent = (props: {
- + - + + busy: Accessor + open: Accessor + directory: string + language: ReturnType + branch: Accessor + workspaceValue: Accessor + workspaceEditActive: Accessor + InlineEditor: WorkspaceSidebarContext["InlineEditor"] + renameWorkspace: WorkspaceSidebarContext["renameWorkspace"] + setEditor: WorkspaceSidebarContext["setEditor"] + projectId?: string +}): JSX.Element => ( +
+
+ }> + + +
+ + {props.local() ? props.language.t("workspace.type.local") : props.language.t("workspace.type.sandbox")} : + + + {props.branch() ?? getFilename(props.directory)} + + } + > + { + const trimmed = next.trim() + if (!trimmed) return + props.renameWorkspace(props.directory, trimmed, props.projectId, props.branch()) + props.setEditor("value", props.workspaceValue()) + }} + class="text-14-medium text-text-base min-w-0 truncate" + displayClass="text-14-medium text-text-base min-w-0 truncate" + editing={props.workspaceEditActive()} + stopPropagation={false} + openOnDblClick={false} + /> + +
+ +
+
+) + +const WorkspaceActions = (props: { + directory: string + local: Accessor + busy: Accessor + menuOpen: Accessor + pendingRename: Accessor + setMenuOpen: (open: boolean) => void + setPendingRename: (value: boolean) => void + sidebarHovering: Accessor + mobile?: boolean + nav: Accessor + touch: Accessor + language: ReturnType + workspaceValue: Accessor + openEditor: WorkspaceSidebarContext["openEditor"] + showResetWorkspaceDialog: WorkspaceSidebarContext["showResetWorkspaceDialog"] + showDeleteWorkspaceDialog: WorkspaceSidebarContext["showDeleteWorkspaceDialog"] + root: string + setHoverSession: WorkspaceSidebarContext["setHoverSession"] + clearHoverProjectSoon: WorkspaceSidebarContext["clearHoverProjectSoon"] + navigateToNewSession: () => void +}): JSX.Element => ( +
+ props.setMenuOpen(open)} + > + + + + + { + if (!props.pendingRename()) return + event.preventDefault() + props.setPendingRename(false) + props.openEditor(`workspace:${props.directory}`, props.workspaceValue()) + }} + > + { + props.setPendingRename(true) + props.setMenuOpen(false) + }} + > + {props.language.t("common.rename")} + + props.showResetWorkspaceDialog(props.root, props.directory)} + > + {props.language.t("common.reset")} + + props.showDeleteWorkspaceDialog(props.root, props.directory)} + > + {props.language.t("common.delete")} + + + + + + + { + event.preventDefault() + event.stopPropagation() + props.setHoverSession(undefined) + props.clearHoverProjectSoon() + props.navigateToNewSession() + }} + /> + + +
+) + +const WorkspaceSessionList = (props: { + slug: Accessor + mobile?: boolean + ctx: WorkspaceSidebarContext + showNew: Accessor + loading: Accessor + sessions: Accessor + children: Accessor> + hasMore: Accessor + loadMore: () => Promise + language: ReturnType +}): JSX.Element => ( + +) + export const SortableWorkspace = (props: { ctx: WorkspaceSidebarContext directory: string @@ -135,46 +351,6 @@ export const SortableWorkspace = (props: { globalSync.child(props.directory, { bootstrap: true }) }) - const header = () => ( -
-
- }> - - -
- - {local() ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")} : - - - {workspaceStore.vcs?.branch ?? getFilename(props.directory)} - - } - > - { - const trimmed = next.trim() - if (!trimmed) return - props.ctx.renameWorkspace(props.directory, trimmed, props.project.id, workspaceStore.vcs?.branch) - props.ctx.setEditor("value", workspaceValue()) - }} - class="text-14-medium text-text-base min-w-0 truncate" - displayClass="text-14-medium text-text-base min-w-0 truncate" - editing={workspaceEditActive()} - stopPropagation={false} - openOnDblClick={false} - /> - -
- -
-
- ) - return (
- {header()} + workspaceStore.vcs?.branch} + workspaceValue={workspaceValue} + workspaceEditActive={workspaceEditActive} + InlineEditor={props.ctx.InlineEditor} + renameWorkspace={props.ctx.renameWorkspace} + setEditor={props.ctx.setEditor} + projectId={props.project.id} + /> } > @@ -211,139 +400,61 @@ export const SortableWorkspace = (props: { menu.open ? "pr-16" : "pr-2" } group-hover/workspace:pr-16 group-focus-within/workspace:pr-16`} > - {header()} + workspaceStore.vcs?.branch} + workspaceValue={workspaceValue} + workspaceEditActive={workspaceEditActive} + InlineEditor={props.ctx.InlineEditor} + renameWorkspace={props.ctx.renameWorkspace} + setEditor={props.ctx.setEditor} + projectId={props.project.id} + />
-
- setMenu("open", open)} - > - - - - - { - if (!menu.pendingRename) return - event.preventDefault() - setMenu("pendingRename", false) - props.ctx.openEditor(`workspace:${props.directory}`, workspaceValue()) - }} - > - { - setMenu("pendingRename", true) - setMenu("open", false) - }} - > - {language.t("common.rename")} - - props.ctx.showResetWorkspaceDialog(props.project.worktree, props.directory)} - > - {language.t("common.reset")} - - props.ctx.showDeleteWorkspaceDialog(props.project.worktree, props.directory)} - > - {language.t("common.delete")} - - - - - - - { - event.preventDefault() - event.stopPropagation() - props.ctx.setHoverSession(undefined) - props.ctx.clearHoverProjectSoon() - navigate(`/${slug()}/session`) - }} - /> - - -
+ menu.open} + pendingRename={() => menu.pendingRename} + setMenuOpen={(open) => setMenu("open", open)} + setPendingRename={(value) => setMenu("pendingRename", value)} + sidebarHovering={props.ctx.sidebarHovering} + mobile={props.mobile} + nav={props.ctx.nav} + touch={touch} + language={language} + workspaceValue={workspaceValue} + openEditor={props.ctx.openEditor} + showResetWorkspaceDialog={props.ctx.showResetWorkspaceDialog} + showDeleteWorkspaceDialog={props.ctx.showDeleteWorkspaceDialog} + root={props.project.worktree} + setHoverSession={props.ctx.setHoverSession} + clearHoverProjectSoon={props.ctx.clearHoverProjectSoon} + navigateToNewSession={() => navigate(`/${slug()}/session`)} + />
- +
-- cgit v1.2.3