Browse Source

refactor(app): detach home views from controllers

Brendan Allan 3 tuần trước cách đây
mục cha
commit
b751e26565

+ 74 - 4
packages/app/src/pages/home.tsx

@@ -18,12 +18,82 @@ export function NewHome() {
         onWheel={sessions.scroll.containOuterWheel}
         onWheel={sessions.scroll.containOuterWheel}
       >
       >
         <div class="mx-auto grid min-h-full w-full max-w-[1080px] grid-rows-[auto_minmax(0,1fr)_auto] gap-4 px-3 lg:grid-cols-[280px_minmax(0,720px)] lg:grid-rows-1 lg:gap-8 lg:px-6">
         <div class="mx-auto grid min-h-full w-full max-w-[1080px] grid-rows-[auto_minmax(0,1fr)_auto] gap-4 px-3 lg:grid-cols-[280px_minmax(0,720px)] lg:grid-rows-1 lg:gap-8 lg:px-6">
-          <HomeProjectsView controller={projects} onWheel={sessions.scroll.containWheel} />
-          <HomeSessionsView controller={sessions} />
+          <HomeProjectsView
+            language={projects.language}
+            servers={projects.servers}
+            projects={projects.projects}
+            recentlyClosed={projects.recentlyClosed}
+            selection={projects.selection}
+            homedir={projects.homedir}
+            serverHealth={projects.serverHealth}
+            projectsForServer={projects.projectsForServer}
+            collapsed={projects.collapsed}
+            menuOpen={projects.menuOpen}
+            canDefaultServer={projects.canDefaultServer}
+            isDefaultServer={projects.isDefaultServer}
+            canRevealProject={projects.canRevealProject}
+            fileManagerActionLabel={projects.fileManagerActionLabel}
+            unseenCount={projects.unseenCount}
+            serverMenuID={projects.serverMenuID}
+            projectMenuID={projects.projectMenuID}
+            onWheel={sessions.scroll.containWheel}
+            onChooseProject={projects.chooseProject}
+            onFocusServer={projects.focusServer}
+            onToggleCollapsed={projects.toggleCollapsed}
+            onEditServer={projects.openEditServer}
+            onSetDefaultServer={projects.setDefaultServer}
+            onRemoveServer={projects.removeServer}
+            onSetMenuOpen={projects.setMenuOpen}
+            onMoveProject={projects.moveProject}
+            onSelectProject={projects.selectProject}
+            onAddProjects={projects.addProjects}
+            onOpenProjectNewSession={projects.openProjectNewSession}
+            onEditProject={projects.editProject}
+            onRevealProject={projects.revealProject}
+            onClearNotifications={projects.clearNotifications}
+            onCloseProject={projects.closeProject}
+            onOpenSettings={projects.openSettings}
+            onOpenHelp={projects.openHelp}
+          />
+          <HomeSessionsView
+            language={sessions.language}
+            groups={sessions.groups}
+            loading={sessions.loading}
+            showProjectName={sessions.showProjectName}
+            server={sessions.server}
+            canCreateSession={sessions.canCreateSession}
+            searchValue={sessions.search.value}
+            searchPlaceholder={sessions.search.placeholder}
+            searchOpen={sessions.search.open}
+            searchLoading={sessions.search.loading}
+            searchResults={sessions.search.results}
+            searchActive={sessions.search.active}
+            searchNoResultsLabel={sessions.search.noResultsLabel}
+            titleOpacity={sessions.header.titleOpacity}
+            isOpenTab={sessions.hasOpenTab}
+            onCreateSession={sessions.createSession}
+            onOpenSession={sessions.openSession}
+            onArchiveSession={sessions.archiveSession}
+            onSetHoverTarget={sessions.scroll.setHoverTarget}
+            onSetThumbTrack={sessions.scroll.setThumbTrack}
+            onSetContent={sessions.header.setContent}
+            onSetHeader={sessions.header.setHeader}
+            onWheel={sessions.scroll.containWheel}
+            onSetSearchRoot={sessions.search.setRoot}
+            onSetSearchInput={sessions.search.setInput}
+            onSetSearchList={sessions.search.setList}
+            onSearchFocus={sessions.search.focus}
+            onSearchInput={sessions.search.input}
+            onSearchClose={sessions.search.close}
+            onSearchMove={sessions.search.move}
+            onSearchSelectActive={sessions.search.selectActive}
+            onSearchHighlight={sessions.search.highlight}
+            onSearchSelect={sessions.search.select}
+          />
           <HomeUtilityNav
           <HomeUtilityNav
             class="flex lg:hidden"
             class="flex lg:hidden"
-            openSettings={projects.openSettings}
-            openHelp={projects.openHelp}
+            onOpenSettings={projects.openSettings}
+            onOpenHelp={projects.openHelp}
             language={projects.language}
             language={projects.language}
           />
           />
         </div>
         </div>

+ 165 - 141
packages/app/src/pages/home/home-projects-view.tsx

@@ -1,4 +1,4 @@
-import { createMemo, For, Show } from "solid-js"
+import { type Accessor, createMemo, For, Show } from "solid-js"
 import { DragDropProvider, PointerSensor } from "@dnd-kit/solid"
 import { DragDropProvider, PointerSensor } from "@dnd-kit/solid"
 import { isSortable, useSortable } from "@dnd-kit/solid/sortable"
 import { isSortable, useSortable } from "@dnd-kit/solid/sortable"
 import { AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom"
 import { AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom"
@@ -10,98 +10,119 @@ import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
 import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
 import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
 import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
 import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
 import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
 import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
-import { getProjectAvatarVariant, type LocalProject } from "@/context/layout"
+import { getProjectAvatarVariant, type HomeProjectSelection, type LocalProject } from "@/context/layout"
 import { ServerConnection } from "@/context/server"
 import { ServerConnection } from "@/context/server"
 import { useLanguage } from "@/context/language"
 import { useLanguage } from "@/context/language"
 import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
 import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
 import { ServerRowMenuView, serverMenuLabels } from "@/components/server/server-row-menu"
 import { ServerRowMenuView, serverMenuLabels } from "@/components/server/server-row-menu"
 import { ServerHealthIndicator } from "@/components/server/server-row"
 import { ServerHealthIndicator } from "@/components/server/server-row"
 import { type ServerHealth } from "@/utils/server-health"
 import { type ServerHealth } from "@/utils/server-health"
-import { type HomeProjectsController } from "./home-projects-controller"
 
 
 const HOME_ROW_LAYOUT =
 const HOME_ROW_LAYOUT =
   "flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] bg-transparent text-left transition-[background-color,color,box-shadow] duration-[120ms] ease-in-out focus-visible:outline-none"
   "flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] bg-transparent text-left transition-[background-color,color,box-shadow] duration-[120ms] ease-in-out focus-visible:outline-none"
 const HOME_PROJECT_NAV_LABEL = "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap"
 const HOME_PROJECT_NAV_LABEL = "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap"
 const HOME_PROJECT_NAV_ROW = `${HOME_ROW_LAYOUT} h-7 gap-2 px-1.5 [font-weight:440] text-v2-text-text-muted hover:bg-v2-background-bg-layer-01 hover:text-v2-text-text-base hover:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)] data-[selected]:bg-v2-background-bg-layer-03 data-[selected]:text-v2-text-text-base data-[selected]:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)] data-[selected]:hover:bg-v2-background-bg-layer-03 focus-visible:bg-v2-background-bg-layer-01 focus-visible:text-v2-text-text-base focus-visible:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)]`
 const HOME_PROJECT_NAV_ROW = `${HOME_ROW_LAYOUT} h-7 gap-2 px-1.5 [font-weight:440] text-v2-text-text-muted hover:bg-v2-background-bg-layer-01 hover:text-v2-text-text-base hover:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)] data-[selected]:bg-v2-background-bg-layer-03 data-[selected]:text-v2-text-text-base data-[selected]:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)] data-[selected]:hover:bg-v2-background-bg-layer-03 focus-visible:bg-v2-background-bg-layer-01 focus-visible:text-v2-text-text-base focus-visible:[box-shadow:inset_0_0_0_0.5px_var(--v2-border-border-muted)]`
 
 
-export function HomeProjectsView(props: { controller: HomeProjectsController; onWheel: (event: WheelEvent) => void }) {
+export type HomeProjectsViewProps = {
+  language: ReturnType<typeof useLanguage>
+  servers: Accessor<ServerConnection.Any[]>
+  projects: Accessor<LocalProject[]>
+  recentlyClosed: Accessor<LocalProject[]>
+  selection: Accessor<HomeProjectSelection>
+  homedir: Accessor<string>
+  serverHealth: (server: ServerConnection.Any) => ServerHealth | undefined
+  projectsForServer: (server: ServerConnection.Any) => LocalProject[]
+  collapsed: (server: ServerConnection.Any) => boolean
+  menuOpen: (id: string) => boolean
+  canDefaultServer: Accessor<boolean>
+  isDefaultServer: (server: ServerConnection.Any) => boolean
+  canRevealProject: (server: ServerConnection.Any) => boolean
+  fileManagerActionLabel: Accessor<string>
+  unseenCount: (server: ServerConnection.Any, project: LocalProject) => number
+  serverMenuID: (server: ServerConnection.Any) => string
+  projectMenuID: (server: ServerConnection.Any, directory: string) => string
+  onWheel: (event: WheelEvent) => void
+  onChooseProject: (server: ServerConnection.Any) => void
+  onFocusServer: (server: ServerConnection.Any) => void
+  onToggleCollapsed: (server: ServerConnection.Any) => void
+  onEditServer: (server: ServerConnection.Http) => void
+  onSetDefaultServer: (server: ServerConnection.Any | undefined) => void
+  onRemoveServer: (server: ServerConnection.Any) => void
+  onSetMenuOpen: (id: string, open: boolean) => void
+  onMoveProject: (server: ServerConnection.Any, worktree: string, index: number) => void
+  onSelectProject: (server: ServerConnection.Any, directory: string) => void
+  onAddProjects: (server: ServerConnection.Any, directories: string[]) => void
+  onOpenProjectNewSession: (server: ServerConnection.Any, directory: string) => void
+  onEditProject: (server: ServerConnection.Any, project: LocalProject) => void
+  onRevealProject: (server: ServerConnection.Any, project: LocalProject) => void
+  onClearNotifications: (server: ServerConnection.Any, project: LocalProject) => void
+  onCloseProject: (server: ServerConnection.Any, directory: string) => void
+  onOpenSettings: () => void
+  onOpenHelp: () => void
+}
+
+export function HomeProjectsView(props: HomeProjectsViewProps) {
   return (
   return (
     <aside
     <aside
       class="mt-6 flex min-h-0 min-w-0 flex-col gap-4 overflow-hidden lg:sticky lg:top-14 lg:mt-14 lg:h-[calc(100cqh-56px)] lg:self-start lg:pt-[52px]"
       class="mt-6 flex min-h-0 min-w-0 flex-col gap-4 overflow-hidden lg:sticky lg:top-14 lg:mt-14 lg:h-[calc(100cqh-56px)] lg:self-start lg:pt-[52px]"
-      aria-label={props.controller.language.t("home.projects")}
+      aria-label={props.language.t("home.projects")}
       onWheel={(event) => {
       onWheel={(event) => {
         if (event.target === event.currentTarget) return
         if (event.target === event.currentTarget) return
         props.onWheel(event)
         props.onWheel(event)
       }}
       }}
     >
     >
       <div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5 pr-3">
       <div class="flex h-7 min-w-0 shrink-0 items-center justify-between pl-1.5 pr-3">
-        <div class="text-v2-text-text-muted [font-weight:530]">{props.controller.language.t("home.projects")}</div>
+        <div class="text-v2-text-text-muted [font-weight:530]">{props.language.t("home.projects")}</div>
         <Show
         <Show
-          when={
-            props.controller.servers().length === 1 &&
-            !(props.controller.projects().length === 0 && props.controller.recentlyClosed().length > 0)
-          }
+          when={props.servers().length === 1 && !(props.projects().length === 0 && props.recentlyClosed().length > 0)}
         >
         >
-          <TooltipV2 placement="bottom" value={props.controller.language.t("home.project.add")}>
+          <TooltipV2 placement="bottom" value={props.language.t("home.project.add")}>
             <IconButtonV2
             <IconButtonV2
               data-action="home-add-project"
               data-action="home-add-project"
               variant="ghost-muted"
               variant="ghost-muted"
               size="large"
               size="large"
               class="titlebar-icon [&_[data-slot=icon-svg]]:text-v2-icon-icon-muted"
               class="titlebar-icon [&_[data-slot=icon-svg]]:text-v2-icon-icon-muted"
               icon={<IconV2 name="folder-add-left" />}
               icon={<IconV2 name="folder-add-left" />}
-              disabled={props.controller.serverHealth(props.controller.servers()[0])?.healthy === false}
-              onClick={() => props.controller.chooseProject(props.controller.servers()[0])}
-              aria-label={props.controller.language.t("home.project.add")}
+              disabled={props.serverHealth(props.servers()[0])?.healthy === false}
+              onClick={() => props.onChooseProject(props.servers()[0])}
+              aria-label={props.language.t("home.project.add")}
             />
             />
           </TooltipV2>
           </TooltipV2>
         </Show>
         </Show>
       </div>
       </div>
       <ScrollView data-slot="home-projects-scroll" class="min-h-0 min-w-0 shrink">
       <ScrollView data-slot="home-projects-scroll" class="min-h-0 min-w-0 shrink">
         <Show
         <Show
-          when={props.controller.servers().length > 1}
+          when={props.servers().length > 1}
           fallback={
           fallback={
             <div class="pr-3">
             <div class="pr-3">
               <Show
               <Show
-                when={props.controller.projects().length > 0}
-                fallback={
-                  <HomeProjectEmpty
-                    controller={props.controller}
-                    server={props.controller.servers()[0]}
-                    recentlyClosed={props.controller.recentlyClosed()}
-                  />
-                }
+                when={props.projects().length > 0}
+                fallback={<HomeProjectEmpty {...props} server={props.servers()[0]} items={props.recentlyClosed()} />}
               >
               >
-                <HomeProjectList
-                  controller={props.controller}
-                  server={props.controller.servers()[0]}
-                  projects={props.controller.projects()}
-                />
+                <HomeProjectList {...props} server={props.servers()[0]} items={props.projects()} />
               </Show>
               </Show>
             </div>
             </div>
           }
           }
         >
         >
           <div class="flex min-w-0 flex-col gap-4 pr-3">
           <div class="flex min-w-0 flex-col gap-4 pr-3">
-            <For each={props.controller.servers()}>
+            <For each={props.servers()}>
               {(item) => {
               {(item) => {
-                const projects = () => props.controller.projectsForServer(item)
-                const healthy = () => !!props.controller.serverHealth(item)?.healthy
+                const projects = () => props.projectsForServer(item)
+                const healthy = () => !!props.serverHealth(item)?.healthy
                 const hasProjects = () => projects().length > 0
                 const hasProjects = () => projects().length > 0
-                const collapsed = () => props.controller.collapsed(item)
+                const collapsed = () => props.collapsed(item)
                 return (
                 return (
                   <div class="flex min-w-0 flex-col gap-1">
                   <div class="flex min-w-0 flex-col gap-1">
                     <HomeServerRow
                     <HomeServerRow
                       server={item}
                       server={item}
-                      controller={props.controller}
-                      selected={
-                        props.controller.selection().server === ServerConnection.key(item) &&
-                        !props.controller.selection().directory
-                      }
+                      {...props}
+                      selected={props.selection().server === ServerConnection.key(item) && !props.selection().directory}
                       collapsed={collapsed()}
                       collapsed={collapsed()}
-                      health={props.controller.serverHealth(item)}
+                      health={props.serverHealth(item)}
                     />
                     />
                     <Show when={healthy() && hasProjects() && !collapsed()}>
                     <Show when={healthy() && hasProjects() && !collapsed()}>
                       <div class="mx-3 h-px bg-v2-border-border-base" />
                       <div class="mx-3 h-px bg-v2-border-border-base" />
-                      <HomeProjectList controller={props.controller} server={item} projects={projects()} />
+                      <HomeProjectList {...props} server={item} items={projects()} />
                     </Show>
                     </Show>
                   </div>
                   </div>
                 )
                 )
@@ -112,9 +133,9 @@ export function HomeProjectsView(props: { controller: HomeProjectsController; on
       </ScrollView>
       </ScrollView>
       <HomeUtilityNav
       <HomeUtilityNav
         class="mb-8 mt-4 hidden shrink-0 lg:flex"
         class="mb-8 mt-4 hidden shrink-0 lg:flex"
-        openSettings={props.controller.openSettings}
-        openHelp={props.controller.openHelp}
-        language={props.controller.language}
+        onOpenSettings={props.onOpenSettings}
+        onOpenHelp={props.onOpenHelp}
+        language={props.language}
       />
       />
     </aside>
     </aside>
   )
   )
@@ -122,8 +143,8 @@ export function HomeProjectsView(props: { controller: HomeProjectsController; on
 
 
 export function HomeUtilityNav(props: {
 export function HomeUtilityNav(props: {
   class?: string
   class?: string
-  openSettings: () => void
-  openHelp: () => void
+  onOpenSettings: () => void
+  onOpenHelp: () => void
   language: ReturnType<typeof useLanguage>
   language: ReturnType<typeof useLanguage>
 }) {
 }) {
   return (
   return (
@@ -131,7 +152,7 @@ export function HomeUtilityNav(props: {
       <button
       <button
         type="button"
         type="button"
         class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
         class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
-        onClick={props.openSettings}
+        onClick={props.onOpenSettings}
       >
       >
         <IconV2 name="settings-gear" size="small" />
         <IconV2 name="settings-gear" size="small" />
         <span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("sidebar.settings")}</span>
         <span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("sidebar.settings")}</span>
@@ -139,7 +160,7 @@ export function HomeUtilityNav(props: {
       <button
       <button
         type="button"
         type="button"
         class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
         class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
-        onClick={props.openHelp}
+        onClick={props.onOpenHelp}
       >
       >
         <IconV2 name="help" size="small" />
         <IconV2 name="help" size="small" />
         <span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("sidebar.help")}</span>
         <span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("sidebar.help")}</span>
@@ -149,15 +170,27 @@ export function HomeUtilityNav(props: {
 }
 }
 
 
 function HomeServerRow(props: {
 function HomeServerRow(props: {
-  controller: HomeProjectsController
+  language: HomeProjectsViewProps["language"]
+  projectsForServer: HomeProjectsViewProps["projectsForServer"]
+  menuOpen: HomeProjectsViewProps["menuOpen"]
+  canDefaultServer: HomeProjectsViewProps["canDefaultServer"]
+  isDefaultServer: HomeProjectsViewProps["isDefaultServer"]
+  serverMenuID: HomeProjectsViewProps["serverMenuID"]
+  onFocusServer: HomeProjectsViewProps["onFocusServer"]
+  onToggleCollapsed: HomeProjectsViewProps["onToggleCollapsed"]
+  onEditServer: HomeProjectsViewProps["onEditServer"]
+  onSetDefaultServer: HomeProjectsViewProps["onSetDefaultServer"]
+  onRemoveServer: HomeProjectsViewProps["onRemoveServer"]
+  onSetMenuOpen: HomeProjectsViewProps["onSetMenuOpen"]
+  onChooseProject: HomeProjectsViewProps["onChooseProject"]
   server: ServerConnection.Any
   server: ServerConnection.Any
   selected: boolean
   selected: boolean
   collapsed: boolean
   collapsed: boolean
   health: ServerHealth | undefined
   health: ServerHealth | undefined
 }) {
 }) {
   const healthy = () => !!props.health?.healthy
   const healthy = () => !!props.health?.healthy
-  const canToggle = () => healthy() && props.controller.projectsForServer(props.server).length > 0
-  const menuID = () => props.controller.serverMenuID(props.server)
+  const canToggle = () => healthy() && props.projectsForServer(props.server).length > 0
+  const menuID = () => props.serverMenuID(props.server)
   return (
   return (
     <div class="group/server relative flex h-7 min-w-0 items-center rounded-[6px]">
     <div class="group/server relative flex h-7 min-w-0 items-center rounded-[6px]">
       <button
       <button
@@ -165,7 +198,7 @@ function HomeServerRow(props: {
         class={`${HOME_PROJECT_NAV_ROW} pr-16 disabled:opacity-60`}
         class={`${HOME_PROJECT_NAV_ROW} pr-16 disabled:opacity-60`}
         data-selected={props.selected ? "" : undefined}
         data-selected={props.selected ? "" : undefined}
         disabled={!healthy()}
         disabled={!healthy()}
-        onClick={() => props.controller.focusServer(props.server)}
+        onClick={() => props.onFocusServer(props.server)}
       >
       >
         <span
         <span
           data-action="home-server-collapse"
           data-action="home-server-collapse"
@@ -175,9 +208,7 @@ function HomeServerRow(props: {
             "cursor-default opacity-40": !canToggle(),
             "cursor-default opacity-40": !canToggle(),
           }}
           }}
           aria-label={
           aria-label={
-            props.collapsed
-              ? props.controller.language.t("home.server.expand")
-              : props.controller.language.t("home.server.collapse")
+            props.collapsed ? props.language.t("home.server.expand") : props.language.t("home.server.collapse")
           }
           }
           aria-disabled={!canToggle()}
           aria-disabled={!canToggle()}
           aria-expanded={canToggle() ? !props.collapsed : undefined}
           aria-expanded={canToggle() ? !props.collapsed : undefined}
@@ -185,7 +216,7 @@ function HomeServerRow(props: {
             event.preventDefault()
             event.preventDefault()
             event.stopPropagation()
             event.stopPropagation()
             if (!canToggle()) return
             if (!canToggle()) return
-            props.controller.toggleCollapsed(props.server)
+            props.onToggleCollapsed(props.server)
           }}
           }}
           onPointerDown={(event) => event.preventDefault()}
           onPointerDown={(event) => event.preventDefault()}
         >
         >
@@ -212,33 +243,29 @@ function HomeServerRow(props: {
       </button>
       </button>
       <div
       <div
         class="hover-reveal absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-1 group-hover/server:opacity-100 focus-within:opacity-100 data-[menu=true]:opacity-100"
         class="hover-reveal absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-1 group-hover/server:opacity-100 focus-within:opacity-100 data-[menu=true]:opacity-100"
-        data-menu={props.controller.menuOpen(menuID())}
+        data-menu={props.menuOpen(menuID())}
       >
       >
         <ServerRowMenuView
         <ServerRowMenuView
           server={props.server}
           server={props.server}
-          labels={serverMenuLabels(props.controller.language)}
-          canDefault={props.controller.canDefaultServer()}
-          isDefault={props.controller.isDefaultServer(props.server)}
-          onEdit={props.controller.openEditServer}
-          onSetDefault={() => props.controller.setDefaultServer(props.server)}
-          onRemoveDefault={() => props.controller.setDefaultServer(undefined)}
-          onRemove={() => props.controller.removeServer(props.server)}
-          open={props.controller.menuOpen(menuID())}
-          onOpenChange={(open) => props.controller.setMenuOpen(menuID(), open)}
+          labels={serverMenuLabels(props.language)}
+          canDefault={props.canDefaultServer()}
+          isDefault={props.isDefaultServer(props.server)}
+          onEdit={props.onEditServer}
+          onSetDefault={() => props.onSetDefaultServer(props.server)}
+          onRemoveDefault={() => props.onSetDefaultServer(undefined)}
+          onRemove={() => props.onRemoveServer(props.server)}
+          open={props.menuOpen(menuID())}
+          onOpenChange={(open) => props.onSetMenuOpen(menuID(), open)}
         />
         />
-        <TooltipV2
-          class="flex shrink-0 items-center"
-          placement="bottom"
-          value={props.controller.language.t("home.project.add")}
-        >
+        <TooltipV2 class="flex shrink-0 items-center" placement="bottom" value={props.language.t("home.project.add")}>
           <IconButtonV2
           <IconButtonV2
             data-action="home-add-project"
             data-action="home-add-project"
             variant="ghost-muted"
             variant="ghost-muted"
             size="small"
             size="small"
             icon={<IconV2 name="folder-add-left" />}
             icon={<IconV2 name="folder-add-left" />}
-            aria-label={props.controller.language.t("home.project.add")}
+            aria-label={props.language.t("home.project.add")}
             disabled={props.health?.healthy === false}
             disabled={props.health?.healthy === false}
-            onClick={() => props.controller.chooseProject(props.server)}
+            onClick={() => props.onChooseProject(props.server)}
           />
           />
         </TooltipV2>
         </TooltipV2>
       </div>
       </div>
@@ -246,10 +273,9 @@ function HomeServerRow(props: {
   )
   )
 }
 }
 
 
-type HomeProjectListProps = {
-  controller: HomeProjectsController
+type HomeProjectListProps = HomeProjectsViewProps & {
   server: ServerConnection.Any
   server: ServerConnection.Any
-  projects: LocalProject[]
+  items: LocalProject[]
 }
 }
 
 
 function HomeProjectList(props: HomeProjectListProps) {
 function HomeProjectList(props: HomeProjectListProps) {
@@ -276,10 +302,9 @@ function HomeProjectList(props: HomeProjectListProps) {
       onDragEnd={(event) => {
       onDragEnd={(event) => {
         const source = event.operation.source
         const source = event.operation.source
         if (event.canceled || !isSortable(source)) return
         if (event.canceled || !isSortable(source)) return
-        if (source.initialIndex !== source.index)
-          props.controller.moveProject(props.server, source.id.toString(), source.index)
-        if (props.controller.selection().server !== ServerConnection.key(props.server))
-          props.controller.selectProject(props.server, source.id.toString())
+        if (source.initialIndex !== source.index) props.onMoveProject(props.server, source.id.toString(), source.index)
+        if (props.selection().server !== ServerConnection.key(props.server))
+          props.onSelectProject(props.server, source.id.toString())
       }}
       }}
     >
     >
       <div class="flex min-w-0 flex-col gap-1" ref={listRef}>
       <div class="flex min-w-0 flex-col gap-1" ref={listRef}>
@@ -288,7 +313,7 @@ function HomeProjectList(props: HomeProjectListProps) {
             remounts all rows — killing any in-flight drag activation (the
             remounts all rows — killing any in-flight drag activation (the
             row's sortable unregisters on unmount) and discarding animations.
             row's sortable unregisters on unmount) and discarding animations.
             String keys keep row elements alive and move them on reorder. */}
             String keys keep row elements alive and move them on reorder. */}
-        <For each={props.projects.map((project) => project.worktree)}>
+        <For each={props.items.map((project) => project.worktree)}>
           {(worktree, index) => <HomeProjectSlot {...props} worktree={worktree} index={index} />}
           {(worktree, index) => <HomeProjectSlot {...props} worktree={worktree} index={index} />}
         </For>
         </For>
       </div>
       </div>
@@ -302,34 +327,35 @@ function HomeProjectSlot(
     index: () => number
     index: () => number
   },
   },
 ) {
 ) {
-  const project = createMemo(() => props.projects.find((item) => item.worktree === props.worktree))
+  const project = createMemo(() => props.items.find((item) => item.worktree === props.worktree))
 
 
   return (
   return (
     <Show when={project()}>
     <Show when={project()}>
       {(item) => (
       {(item) => (
         <HomeProjectRow
         <HomeProjectRow
-          controller={props.controller}
+          {...props}
           project={item()}
           project={item()}
           server={props.server}
           server={props.server}
           index={props.index}
           index={props.index}
-          serverSelected={props.controller.selection().server === ServerConnection.key(props.server)}
+          serverSelected={props.selection().server === ServerConnection.key(props.server)}
           selected={
           selected={
-            props.controller.selection().server === ServerConnection.key(props.server) &&
-            props.controller.selection().directory === props.worktree
+            props.selection().server === ServerConnection.key(props.server) &&
+            props.selection().directory === props.worktree
           }
           }
-          unseenCount={props.controller.unseenCount(props.server, item())}
+          unseen={props.unseenCount(props.server, item())}
         />
         />
       )}
       )}
     </Show>
     </Show>
   )
   )
 }
 }
 
 
-function HomeProjectEmpty(props: {
-  controller: HomeProjectsController
-  server: ServerConnection.Any
-  recentlyClosed: LocalProject[]
-}) {
-  const unreachable = () => props.controller.serverHealth(props.server)?.healthy === false
+function HomeProjectEmpty(
+  props: HomeProjectsViewProps & {
+    server: ServerConnection.Any
+    items: LocalProject[]
+  },
+) {
+  const unreachable = () => props.serverHealth(props.server)?.healthy === false
   return (
   return (
     <div class="flex min-w-0 flex-col gap-1">
     <div class="flex min-w-0 flex-col gap-1">
       <button
       <button
@@ -337,33 +363,32 @@ function HomeProjectEmpty(props: {
         data-action="home-add-project-row"
         data-action="home-add-project-row"
         class={`${HOME_PROJECT_NAV_ROW} disabled:opacity-60 [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
         class={`${HOME_PROJECT_NAV_ROW} disabled:opacity-60 [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
         disabled={unreachable()}
         disabled={unreachable()}
-        onClick={() => props.controller.chooseProject(props.server)}
+        onClick={() => props.onChooseProject(props.server)}
       >
       >
         <IconV2 name="folder-add-left" size="small" />
         <IconV2 name="folder-add-left" size="small" />
-        <span class={HOME_PROJECT_NAV_LABEL}>{props.controller.language.t("home.project.add")}</span>
+        <span class={HOME_PROJECT_NAV_LABEL}>{props.language.t("home.project.add")}</span>
       </button>
       </button>
-      <Show when={props.recentlyClosed.length > 0}>
+      <Show when={props.items.length > 0}>
         <div class="mt-3 flex h-7 min-w-0 shrink-0 items-center pl-1.5 pr-3">
         <div class="mt-3 flex h-7 min-w-0 shrink-0 items-center pl-1.5 pr-3">
-          <div class="text-v2-text-text-faint [font-weight:530]">
-            {props.controller.language.t("home.recentlyClosed")}
-          </div>
+          <div class="text-v2-text-text-faint [font-weight:530]">{props.language.t("home.recentlyClosed")}</div>
         </div>
         </div>
-        <For each={props.recentlyClosed}>
-          {(project) => <HomeRecentlyClosedRow project={project} controller={props.controller} server={props.server} />}
+        <For each={props.items}>
+          {(project) => <HomeRecentlyClosedRow {...props} project={project} server={props.server} />}
         </For>
         </For>
       </Show>
       </Show>
     </div>
     </div>
   )
   )
 }
 }
 
 
-function HomeRecentlyClosedRow(props: {
-  controller: HomeProjectsController
-  project: LocalProject
-  server: ServerConnection.Any
-}) {
-  const unreachable = () => props.controller.serverHealth(props.server)?.healthy === false
+function HomeRecentlyClosedRow(
+  props: HomeProjectsViewProps & {
+    project: LocalProject
+    server: ServerConnection.Any
+  },
+) {
+  const unreachable = () => props.serverHealth(props.server)?.healthy === false
   const path = () => {
   const path = () => {
-    const home = props.controller.homedir()
+    const home = props.homedir()
     const worktree = props.project.worktree
     const worktree = props.project.worktree
     if (home && (worktree === home || worktree.startsWith(`${home}/`))) return `~${worktree.slice(home.length)}`
     if (home && (worktree === home || worktree.startsWith(`${home}/`))) return `~${worktree.slice(home.length)}`
     return worktree
     return worktree
@@ -375,7 +400,7 @@ function HomeRecentlyClosedRow(props: {
         data-component="home-recently-closed-row"
         data-component="home-recently-closed-row"
         class={`${HOME_PROJECT_NAV_ROW} disabled:opacity-60`}
         class={`${HOME_PROJECT_NAV_ROW} disabled:opacity-60`}
         disabled={unreachable()}
         disabled={unreachable()}
-        onClick={() => props.controller.addProjects(props.server, [props.project.worktree])}
+        onClick={() => props.onAddProjects(props.server, [props.project.worktree])}
       >
       >
         <HomeProjectAvatar project={props.project} outline />
         <HomeProjectAvatar project={props.project} outline />
         <span class={HOME_PROJECT_NAV_LABEL}>{displayName(props.project)}</span>
         <span class={HOME_PROJECT_NAV_LABEL}>{displayName(props.project)}</span>
@@ -384,16 +409,17 @@ function HomeRecentlyClosedRow(props: {
   )
   )
 }
 }
 
 
-function HomeProjectRow(props: {
-  controller: HomeProjectsController
-  project: LocalProject
-  server: ServerConnection.Any
-  index: () => number
-  serverSelected: boolean
-  selected: boolean
-  unseenCount: number
-}) {
-  const serverUnreachable = () => props.controller.serverHealth(props.server)?.healthy === false
+function HomeProjectRow(
+  props: HomeProjectsViewProps & {
+    project: LocalProject
+    server: ServerConnection.Any
+    index: () => number
+    serverSelected: boolean
+    selected: boolean
+    unseen: number
+  },
+) {
+  const serverUnreachable = () => props.serverHealth(props.server)?.healthy === false
   const sortable = useSortable({
   const sortable = useSortable({
     get id() {
     get id() {
       return props.project.worktree
       return props.project.worktree
@@ -403,7 +429,7 @@ function HomeProjectRow(props: {
     },
     },
   })
   })
   let pointerDownSelected: boolean | undefined
   let pointerDownSelected: boolean | undefined
-  const menuID = () => props.controller.projectMenuID(props.server, props.project.worktree)
+  const menuID = () => props.projectMenuID(props.server, props.project.worktree)
   return (
   return (
     <div
     <div
       ref={sortable.ref}
       ref={sortable.ref}
@@ -432,7 +458,7 @@ function HomeProjectRow(props: {
           if (event.button !== 0 || event.pointerType === "touch") return
           if (event.button !== 0 || event.pointerType === "touch") return
           if (!props.serverSelected) return
           if (!props.serverSelected) return
           pointerDownSelected = props.selected
           pointerDownSelected = props.selected
-          if (!props.selected) props.controller.selectProject(props.server, props.project.worktree)
+          if (!props.selected) props.onSelectProject(props.server, props.project.worktree)
         }}
         }}
         onClick={(event) => {
         onClick={(event) => {
           // The drag sensor calls preventDefault on post-drag clicks; never
           // The drag sensor calls preventDefault on post-drag clicks; never
@@ -440,12 +466,12 @@ function HomeProjectRow(props: {
           if (event.defaultPrevented) return
           if (event.defaultPrevented) return
           // Keyboard activation and touch taps keep the original toggle.
           // Keyboard activation and touch taps keep the original toggle.
           if (event.detail === 0 || pointerDownSelected === undefined) {
           if (event.detail === 0 || pointerDownSelected === undefined) {
-            props.controller.selectProject(props.server, props.project.worktree)
+            props.onSelectProject(props.server, props.project.worktree)
             return
             return
           }
           }
           // Mouse: pointerdown already selected unselected rows; a plain click
           // Mouse: pointerdown already selected unselected rows; a plain click
           // on an already-selected row toggles it off.
           // on an already-selected row toggles it off.
-          if (pointerDownSelected) props.controller.selectProject(props.server, props.project.worktree)
+          if (pointerDownSelected) props.onSelectProject(props.server, props.project.worktree)
           pointerDownSelected = undefined
           pointerDownSelected = undefined
         }}
         }}
       >
       >
@@ -454,14 +480,14 @@ function HomeProjectRow(props: {
       </button>
       </button>
       <div
       <div
         class="hover-reveal absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-1 group-hover/project:opacity-100 focus-within:opacity-100 data-[menu=true]:opacity-100"
         class="hover-reveal absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-1 group-hover/project:opacity-100 focus-within:opacity-100 data-[menu=true]:opacity-100"
-        data-menu={props.controller.menuOpen(menuID())}
+        data-menu={props.menuOpen(menuID())}
       >
       >
         <MenuV2
         <MenuV2
           gutter={6}
           gutter={6}
           modal={false}
           modal={false}
           placement="bottom-end"
           placement="bottom-end"
-          open={props.controller.menuOpen(menuID())}
-          onOpenChange={(open) => props.controller.setMenuOpen(menuID(), open)}
+          open={props.menuOpen(menuID())}
+          onOpenChange={(open) => props.onSetMenuOpen(menuID(), open)}
         >
         >
           <MenuV2.Trigger
           <MenuV2.Trigger
             as={IconButtonV2}
             as={IconButtonV2}
@@ -469,32 +495,30 @@ function HomeProjectRow(props: {
             variant="ghost-muted"
             variant="ghost-muted"
             size="small"
             size="small"
             icon={<IconV2 name="outline-dots" />}
             icon={<IconV2 name="outline-dots" />}
-            aria-label={props.controller.language.t("common.moreOptions")}
+            aria-label={props.language.t("common.moreOptions")}
           />
           />
           <MenuV2.Portal>
           <MenuV2.Portal>
             <MenuV2.Content>
             <MenuV2.Content>
-              <MenuV2.Item
-                onSelect={() => props.controller.openProjectNewSession(props.server, props.project.worktree)}
-              >
-                {props.controller.language.t("command.session.new")}
+              <MenuV2.Item onSelect={() => props.onOpenProjectNewSession(props.server, props.project.worktree)}>
+                {props.language.t("command.session.new")}
               </MenuV2.Item>
               </MenuV2.Item>
-              <MenuV2.Item onSelect={() => props.controller.editProject(props.server, props.project)}>
-                {props.controller.language.t("dialog.project.edit.title")}
+              <MenuV2.Item onSelect={() => props.onEditProject(props.server, props.project)}>
+                {props.language.t("dialog.project.edit.title")}
               </MenuV2.Item>
               </MenuV2.Item>
-              <Show when={props.controller.canRevealProject(props.server)}>
-                <MenuV2.Item onSelect={() => props.controller.revealProject(props.server, props.project)}>
-                  {props.controller.fileManagerActionLabel()}
+              <Show when={props.canRevealProject(props.server)}>
+                <MenuV2.Item onSelect={() => props.onRevealProject(props.server, props.project)}>
+                  {props.fileManagerActionLabel()}
                 </MenuV2.Item>
                 </MenuV2.Item>
               </Show>
               </Show>
               <MenuV2.Item
               <MenuV2.Item
-                disabled={props.unseenCount === 0}
-                onSelect={() => props.controller.clearNotifications(props.server, props.project)}
+                disabled={props.unseen === 0}
+                onSelect={() => props.onClearNotifications(props.server, props.project)}
               >
               >
-                {props.controller.language.t("sidebar.project.clearNotifications")}
+                {props.language.t("sidebar.project.clearNotifications")}
               </MenuV2.Item>
               </MenuV2.Item>
               <MenuV2.Separator />
               <MenuV2.Separator />
-              <MenuV2.Item onSelect={() => props.controller.closeProject(props.server, props.project.worktree)}>
-                {props.controller.language.t("common.close")}
+              <MenuV2.Item onSelect={() => props.onCloseProject(props.server, props.project.worktree)}>
+                {props.language.t("common.close")}
               </MenuV2.Item>
               </MenuV2.Item>
             </MenuV2.Content>
             </MenuV2.Content>
           </MenuV2.Portal>
           </MenuV2.Portal>
@@ -504,8 +528,8 @@ function HomeProjectRow(props: {
           variant="ghost-muted"
           variant="ghost-muted"
           size="small"
           size="small"
           icon={<IconV2 name="edit" />}
           icon={<IconV2 name="edit" />}
-          aria-label={props.controller.language.t("command.session.new")}
-          onClick={() => props.controller.openProjectNewSession(props.server, props.project.worktree)}
+          aria-label={props.language.t("command.session.new")}
+          onClick={() => props.onOpenProjectNewSession(props.server, props.project.worktree)}
         />
         />
       </div>
       </div>
     </div>
     </div>

+ 4 - 3
packages/app/src/pages/home/home-sessions-controller.tsx

@@ -560,18 +560,19 @@ function createHomeSessionHeaderController(groups: () => HomeSessionGroup[]) {
 export type HomeSessionsController = ReturnType<typeof createHomeSessionsController>
 export type HomeSessionsController = ReturnType<typeof createHomeSessionsController>
 
 
 export function HomeSessionStatusController(props: {
 export function HomeSessionStatusController(props: {
-  controller: HomeSessionsController
+  server: Accessor<ServerConnection.Key>
   record: HomeSessionRecord
   record: HomeSessionRecord
+  isOpenTab: (record: HomeSessionRecord) => boolean
   render: (state: { unread: Accessor<boolean>; loading: Accessor<boolean>; open: Accessor<boolean> }) => JSX.Element
   render: (state: { unread: Accessor<boolean>; loading: Accessor<boolean>; open: Accessor<boolean> }) => JSX.Element
 }) {
 }) {
   const avatar = useSessionTabAvatarState(
   const avatar = useSessionTabAvatarState(
-    props.controller.server,
+    props.server,
     () => props.record.session.directory,
     () => props.record.session.directory,
     () => props.record.session.id,
     () => props.record.session.id,
   )
   )
   return props.render({
   return props.render({
     unread: avatar.unread,
     unread: avatar.unread,
     loading: avatar.loading,
     loading: avatar.loading,
-    open: () => props.controller.hasOpenTab(props.record),
+    open: () => props.isOpenTab(props.record),
   })
   })
 }
 }

+ 117 - 82
packages/app/src/pages/home/home-sessions-view.tsx

@@ -1,4 +1,5 @@
-import { type ComponentProps, createMemo, For, Show } from "solid-js"
+import type { Session } from "@opencode-ai/sdk/v2/client"
+import { type Accessor, createMemo, For, Show } from "solid-js"
 import { Spinner } from "@opencode-ai/ui/spinner"
 import { Spinner } from "@opencode-ai/ui/spinner"
 import { ScrollView } from "@opencode-ai/ui/scroll-view"
 import { ScrollView } from "@opencode-ai/ui/scroll-view"
 import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
 import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
@@ -6,14 +7,16 @@ import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
 import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
 import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
 import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
 import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
 import { useLanguage } from "@/context/language"
 import { useLanguage } from "@/context/language"
+import { ServerConnection } from "@/context/server"
 import { SessionTabAvatarView } from "@/pages/layout/session-tab-avatar"
 import { SessionTabAvatarView } from "@/pages/layout/session-tab-avatar"
 import { sessionTitle } from "@/utils/session-title"
 import { sessionTitle } from "@/utils/session-title"
 import { shouldOpenSessionInBackground } from "../home-session-open"
 import { shouldOpenSessionInBackground } from "../home-session-open"
 import {
 import {
   HomeSessionStatusController,
   HomeSessionStatusController,
   homeSessionSearchKey,
   homeSessionSearchKey,
+  type HomeSessionGroup,
   type HomeSessionRecord,
   type HomeSessionRecord,
-  type HomeSessionsController,
+  type OpenSessionOptions,
 } from "./home-sessions-controller"
 } from "./home-sessions-controller"
 
 
 const SHOW_HOME_SESSION_ARCHIVE = false
 const SHOW_HOME_SESSION_ARCHIVE = false
@@ -43,19 +46,52 @@ function isBackgroundOpen(event: MouseEvent) {
   })
   })
 }
 }
 
 
-export function HomeSessionsView(props: { controller: HomeSessionsController }) {
+export type HomeSessionsViewProps = {
+  language: ReturnType<typeof useLanguage>
+  groups: Accessor<HomeSessionGroup[]>
+  loading: Accessor<boolean>
+  showProjectName: Accessor<boolean>
+  server: Accessor<ServerConnection.Key>
+  canCreateSession: Accessor<boolean>
+  searchValue: Accessor<string>
+  searchPlaceholder: Accessor<string>
+  searchOpen: Accessor<boolean>
+  searchLoading: Accessor<boolean>
+  searchResults: Accessor<HomeSessionRecord[]>
+  searchActive: Accessor<string>
+  searchNoResultsLabel: Accessor<string>
+  titleOpacity: (id: HomeSessionGroup["id"]) => number
+  isOpenTab: (record: HomeSessionRecord) => boolean
+  onCreateSession: () => void
+  onOpenSession: (session: Session, options?: OpenSessionOptions) => void
+  onArchiveSession: (session: Session) => Promise<void>
+  onSetHoverTarget: (element: HTMLElement) => void
+  onSetThumbTrack: (element: HTMLDivElement) => void
+  onSetContent: (element: HTMLDivElement) => void
+  onSetHeader: (id: HomeSessionGroup["id"], element: HTMLDivElement) => void
+  onWheel: (event: WheelEvent) => void
+  onSetSearchRoot: (element: HTMLDivElement) => void
+  onSetSearchInput: (element: HTMLInputElement) => void
+  onSetSearchList: (element: HTMLDivElement) => void
+  onSearchFocus: () => void
+  onSearchInput: (value: string) => void
+  onSearchClose: () => void
+  onSearchMove: (delta: number) => void
+  onSearchSelectActive: () => void
+  onSearchHighlight: (record: HomeSessionRecord) => void
+  onSearchSelect: (record: HomeSessionRecord, options?: OpenSessionOptions) => void
+}
+
+export function HomeSessionsView(props: HomeSessionsViewProps) {
   return (
   return (
     <section
     <section
-      ref={props.controller.scroll.setHoverTarget}
+      ref={props.onSetHoverTarget}
       class="min-h-0 min-w-0 flex-1 flex flex-col"
       class="min-h-0 min-w-0 flex-1 flex flex-col"
-      aria-label={props.controller.language.t("sidebar.project.recentSessions")}
+      aria-label={props.language.t("sidebar.project.recentSessions")}
     >
     >
-      <div
-        class="sticky top-0 z-30 shrink-0 bg-v2-background-bg-base pb-3 pt-6 lg:pt-12"
-        onWheel={props.controller.scroll.containWheel}
-      >
-        <HomeSessionSearch controller={props.controller} />
-        <Show when={props.controller.groups().length > 0 && props.controller.canCreateSession()}>
+      <div class="sticky top-0 z-30 shrink-0 bg-v2-background-bg-base pb-3 pt-6 lg:pt-12" onWheel={props.onWheel}>
+        <HomeSessionSearch {...props} />
+        <Show when={props.groups().length > 0 && props.canCreateSession()}>
           <div class="pointer-events-none absolute right-0 top-[84px] z-20 flex lg:top-[108px]">
           <div class="pointer-events-none absolute right-0 top-[84px] z-20 flex lg:top-[108px]">
             <ButtonV2
             <ButtonV2
               data-action="home-new-session"
               data-action="home-new-session"
@@ -63,54 +99,52 @@ export function HomeSessionsView(props: { controller: HomeSessionsController })
               size="normal"
               size="normal"
               icon="edit"
               icon="edit"
               class="pointer-events-auto h-7 px-2 [font-weight:530]"
               class="pointer-events-auto h-7 px-2 [font-weight:530]"
-              onClick={props.controller.createSession}
+              onClick={props.onCreateSession}
             >
             >
-              {props.controller.language.t("command.session.new")}
+              {props.language.t("command.session.new")}
             </ButtonV2>
             </ButtonV2>
           </div>
           </div>
         </Show>
         </Show>
       </div>
       </div>
       <div class="pointer-events-none sticky top-[84px] z-40 h-0 -mr-3 lg:top-[108px]">
       <div class="pointer-events-none sticky top-[84px] z-40 h-0 -mr-3 lg:top-[108px]">
         <div
         <div
-          ref={props.controller.scroll.setThumbTrack}
+          ref={props.onSetThumbTrack}
           data-component="home-session-scroll-track"
           data-component="home-session-scroll-track"
           class="relative ml-auto h-[calc(100cqh-84px)] w-3 lg:h-[calc(100cqh-108px)]"
           class="relative ml-auto h-[calc(100cqh-84px)] w-3 lg:h-[calc(100cqh-108px)]"
         />
         />
       </div>
       </div>
       <div class="-mr-3 min-h-[calc(100cqh-72px)] lg:min-h-[calc(100cqh-96px)]">
       <div class="-mr-3 min-h-[calc(100cqh-72px)] lg:min-h-[calc(100cqh-96px)]">
         <Show
         <Show
-          when={!props.controller.loading()}
+          when={!props.loading()}
           fallback={
           fallback={
             <div class="pt-3">
             <div class="pt-3">
-              <HomeSessionSkeleton label={props.controller.language.t("common.loading")} />
+              <HomeSessionSkeleton label={props.language.t("common.loading")} />
             </div>
             </div>
           }
           }
         >
         >
           <Show
           <Show
-            when={props.controller.groups().length > 0}
+            when={props.groups().length > 0}
             fallback={
             fallback={
               <HomeSessionsEmpty
               <HomeSessionsEmpty
-                onNewSession={props.controller.canCreateSession() ? props.controller.createSession : undefined}
-                language={props.controller.language}
+                onNewSession={props.canCreateSession() ? props.onCreateSession : undefined}
+                language={props.language}
               />
               />
             }
             }
           >
           >
-            <div ref={props.controller.header.setContent} class="flex flex-col pt-3 pr-3 pb-16">
-              <For each={props.controller.groups()}>
+            <div ref={props.onSetContent} class="flex flex-col pt-3 pr-3 pb-16">
+              <For each={props.groups()}>
                 {(group, index) => (
                 {(group, index) => (
                   <>
                   <>
                     <HomeSessionGroupHeader
                     <HomeSessionGroupHeader
                       title={group.title}
                       title={group.title}
-                      titleOpacity={props.controller.header.titleOpacity(group.id)}
-                      ref={(element) => props.controller.header.setHeader(group.id, element)}
+                      titleOpacity={props.titleOpacity(group.id)}
+                      onSetRef={(element) => props.onSetHeader(group.id, element)}
                       elevated={index() === 0}
                       elevated={index() === 0}
                     />
                     />
                     <div
                     <div
-                      class={`flex min-w-0 flex-col gap-px pt-4 ${index() === props.controller.groups().length - 1 ? "" : "mb-6"}`}
+                      class={`flex min-w-0 flex-col gap-px pt-4 ${index() === props.groups().length - 1 ? "" : "mb-6"}`}
                     >
                     >
-                      <For each={group.sessions}>
-                        {(record) => <HomeSessionRow controller={props.controller} record={record} />}
-                      </For>
+                      <For each={group.sessions}>{(record) => <HomeSessionRow {...props} record={record} />}</For>
                     </div>
                     </div>
                   </>
                   </>
                 )}
                 )}
@@ -124,14 +158,16 @@ export function HomeSessionsView(props: { controller: HomeSessionsController })
 }
 }
 
 
 function HomeSessionLeadingController(props: {
 function HomeSessionLeadingController(props: {
-  controller: HomeSessionsController
+  server: HomeSessionsViewProps["server"]
+  isOpenTab: HomeSessionsViewProps["isOpenTab"]
   record: HomeSessionRecord
   record: HomeSessionRecord
   revealProjectOnHover: boolean
   revealProjectOnHover: boolean
 }) {
 }) {
   return (
   return (
     <HomeSessionStatusController
     <HomeSessionStatusController
-      controller={props.controller}
+      server={props.server}
       record={props.record}
       record={props.record}
+      isOpenTab={props.isOpenTab}
       render={(state) => (
       render={(state) => (
         <HomeSessionLeading
         <HomeSessionLeading
           record={props.record}
           record={props.record}
@@ -172,11 +208,11 @@ function HomeSessionLeading(props: {
   )
   )
 }
 }
 
 
-function HomeSessionSearch(props: { controller: HomeSessionsController }) {
+function HomeSessionSearch(props: HomeSessionsViewProps) {
   return (
   return (
     <div class="w-full">
     <div class="w-full">
-      <div ref={props.controller.search.setRoot} data-component="home-session-search" class="relative z-30 w-full">
-        <Show when={props.controller.search.open()}>
+      <div ref={props.onSetSearchRoot} data-component="home-session-search" class="relative z-30 w-full">
+        <Show when={props.searchOpen()}>
           <div
           <div
             data-component="home-session-search-panel"
             data-component="home-session-search-panel"
             class="absolute flex flex-col overflow-hidden rounded-[12px] bg-v2-background-bg-base shadow-[var(--v2-elevation-floating)]"
             class="absolute flex flex-col overflow-hidden rounded-[12px] bg-v2-background-bg-base shadow-[var(--v2-elevation-floating)]"
@@ -185,7 +221,7 @@ function HomeSessionSearch(props: { controller: HomeSessionsController }) {
             <div class="flex flex-col pt-9">
             <div class="flex flex-col pt-9">
               <div id={HOME_SESSION_SEARCH_RESULTS_ID} role="listbox" class="flex flex-col gap-4 pt-4">
               <div id={HOME_SESSION_SEARCH_RESULTS_ID} role="listbox" class="flex flex-col gap-4 pt-4">
                 <Show
                 <Show
-                  when={!props.controller.search.loading()}
+                  when={!props.searchLoading()}
                   fallback={
                   fallback={
                     <div class="flex items-center justify-center px-4 py-3 text-v2-text-text-muted [font-weight:440]">
                     <div class="flex items-center justify-center px-4 py-3 text-v2-text-text-muted [font-weight:440]">
                       <Spinner class="size-4" />
                       <Spinner class="size-4" />
@@ -193,25 +229,25 @@ function HomeSessionSearch(props: { controller: HomeSessionsController }) {
                   }
                   }
                 >
                 >
                   <Show
                   <Show
-                    when={props.controller.search.results().length > 0}
+                    when={props.searchResults().length > 0}
                     fallback={
                     fallback={
                       <p class="my-1.5 px-4 pb-2 text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-muted [font-weight:440]">
                       <p class="my-1.5 px-4 pb-2 text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-muted [font-weight:440]">
-                        {props.controller.search.noResultsLabel()}
+                        {props.searchNoResultsLabel()}
                       </p>
                       </p>
                     }
                     }
                   >
                   >
                     <div class="flex flex-col">
                     <div class="flex flex-col">
                       <p class="my-1.5 pl-[18px] pr-6 text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-muted [font-weight:440]">
                       <p class="my-1.5 pl-[18px] pr-6 text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-muted [font-weight:440]">
-                        {props.controller.language.t("home.sessions.search.sessions")}
+                        {props.language.t("home.sessions.search.sessions")}
                       </p>
                       </p>
-                      <ScrollView class="max-h-80" viewportRef={props.controller.search.setList}>
+                      <ScrollView class="max-h-80" viewportRef={props.onSetSearchList}>
                         <div class="flex flex-col gap-px pb-2">
                         <div class="flex flex-col gap-px pb-2">
-                          <For each={props.controller.search.results()}>
+                          <For each={props.searchResults()}>
                             {(record) => (
                             {(record) => (
                               <HomeSessionSearchResultRow
                               <HomeSessionSearchResultRow
-                                controller={props.controller}
+                                {...props}
                                 record={record}
                                 record={record}
-                                selected={props.controller.search.active() === homeSessionSearchKey(record)}
+                                selected={props.searchActive() === homeSessionSearchKey(record)}
                               />
                               />
                             )}
                             )}
                           </For>
                           </For>
@@ -227,57 +263,57 @@ function HomeSessionSearch(props: { controller: HomeSessionsController }) {
         <label class="relative z-20 flex h-9 w-full items-center gap-2 rounded-[6px] bg-v2-background-bg-layer-02/60 py-1 pl-3 pr-2 text-v2-icon-icon-muted transition-[background-color,box-shadow] duration-[120ms] ease-in-out hover:bg-v2-background-bg-layer-02 focus-within:bg-v2-background-bg-layer-02">
         <label class="relative z-20 flex h-9 w-full items-center gap-2 rounded-[6px] bg-v2-background-bg-layer-02/60 py-1 pl-3 pr-2 text-v2-icon-icon-muted transition-[background-color,box-shadow] duration-[120ms] ease-in-out hover:bg-v2-background-bg-layer-02 focus-within:bg-v2-background-bg-layer-02">
           <IconV2 name="magnifying-glass" />
           <IconV2 name="magnifying-glass" />
           <input
           <input
-            ref={props.controller.search.setInput}
+            ref={props.onSetSearchInput}
             class="relative z-20 min-w-0 flex-1 border-0 bg-transparent text-v2-text-text-base outline-0 [font-weight:440] placeholder:text-v2-text-text-faint"
             class="relative z-20 min-w-0 flex-1 border-0 bg-transparent text-v2-text-text-base outline-0 [font-weight:440] placeholder:text-v2-text-text-faint"
-            value={props.controller.search.value()}
-            placeholder={props.controller.search.placeholder()}
-            aria-label={props.controller.search.placeholder()}
-            aria-expanded={props.controller.search.open()}
+            value={props.searchValue()}
+            placeholder={props.searchPlaceholder()}
+            aria-label={props.searchPlaceholder()}
+            aria-expanded={props.searchOpen()}
             aria-controls={HOME_SESSION_SEARCH_RESULTS_ID}
             aria-controls={HOME_SESSION_SEARCH_RESULTS_ID}
             aria-autocomplete="list"
             aria-autocomplete="list"
             aria-activedescendant={
             aria-activedescendant={
-              props.controller.search.active() && props.controller.search.open()
-                ? `home-session-search-option-${props.controller.search.active()}`
+              props.searchActive() && props.searchOpen()
+                ? `home-session-search-option-${props.searchActive()}`
                 : undefined
                 : undefined
             }
             }
-            onFocus={props.controller.search.focus}
-            onInput={(event) => props.controller.search.input(event.currentTarget.value)}
+            onFocus={props.onSearchFocus}
+            onInput={(event) => props.onSearchInput(event.currentTarget.value)}
             onKeyDown={(event) => {
             onKeyDown={(event) => {
               if (event.key === "Escape") {
               if (event.key === "Escape") {
                 event.preventDefault()
                 event.preventDefault()
-                props.controller.search.close()
+                props.onSearchClose()
                 event.currentTarget.blur()
                 event.currentTarget.blur()
                 return
                 return
               }
               }
-              if (!props.controller.search.open() || props.controller.search.results().length === 0) return
+              if (!props.searchOpen() || props.searchResults().length === 0) return
               if (event.altKey || event.metaKey) return
               if (event.altKey || event.metaKey) return
               if (event.key === "ArrowDown") {
               if (event.key === "ArrowDown") {
                 event.preventDefault()
                 event.preventDefault()
-                props.controller.search.move(1)
+                props.onSearchMove(1)
                 return
                 return
               }
               }
               if (event.key === "ArrowUp") {
               if (event.key === "ArrowUp") {
                 event.preventDefault()
                 event.preventDefault()
-                props.controller.search.move(-1)
+                props.onSearchMove(-1)
                 return
                 return
               }
               }
               if (event.key === "Enter" && !event.isComposing) {
               if (event.key === "Enter" && !event.isComposing) {
                 event.preventDefault()
                 event.preventDefault()
-                props.controller.search.selectActive()
+                props.onSearchSelectActive()
               }
               }
             }}
             }}
           />
           />
-          <Show when={props.controller.search.value()}>
+          <Show when={props.searchValue()}>
             <IconButtonV2
             <IconButtonV2
               type="button"
               type="button"
               variant="ghost-muted"
               variant="ghost-muted"
               size="small"
               size="small"
               class="relative z-20 shrink-0"
               class="relative z-20 shrink-0"
               icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
               icon={<IconV2 name="close" size="large" class="text-v2-icon-icon-muted" />}
-              aria-label={props.controller.search.placeholder()}
+              aria-label={props.searchPlaceholder()}
               onClick={() => {
               onClick={() => {
-                props.controller.search.close()
-                props.controller.search.focus()
+                props.onSearchClose()
+                props.onSearchFocus()
               }}
               }}
             />
             />
           </Show>
           </Show>
@@ -287,13 +323,14 @@ function HomeSessionSearch(props: { controller: HomeSessionsController }) {
   )
   )
 }
 }
 
 
-function HomeSessionSearchResultRow(props: {
-  controller: HomeSessionsController
-  record: HomeSessionRecord
-  selected: boolean
-}) {
+function HomeSessionSearchResultRow(
+  props: HomeSessionsViewProps & {
+    record: HomeSessionRecord
+    selected: boolean
+  },
+) {
   const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
   const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
-  const showProjectName = () => props.controller.showProjectName() && props.record.projectName
+  const showProjectName = () => props.showProjectName() && props.record.projectName
   const key = () => homeSessionSearchKey(props.record)
   const key = () => homeSessionSearchKey(props.record)
 
 
   return (
   return (
@@ -309,19 +346,20 @@ function HomeSessionSearchResultRow(props: {
         "bg-v2-overlay-simple-overlay-hover": props.selected,
         "bg-v2-overlay-simple-overlay-hover": props.selected,
         group: !!showProjectName(),
         group: !!showProjectName(),
       }}
       }}
-      onMouseEnter={() => props.controller.search.highlight(props.record)}
+      onMouseEnter={() => props.onSearchHighlight(props.record)}
       onMouseDown={(event) => {
       onMouseDown={(event) => {
         if (event.button === 1) event.preventDefault()
         if (event.button === 1) event.preventDefault()
       }}
       }}
-      onClick={(event) => props.controller.search.select(props.record, { background: isBackgroundOpen(event) })}
+      onClick={(event) => props.onSearchSelect(props.record, { background: isBackgroundOpen(event) })}
       onAuxClick={(event) => {
       onAuxClick={(event) => {
         if (!isBackgroundOpen(event)) return
         if (!isBackgroundOpen(event)) return
         event.preventDefault()
         event.preventDefault()
-        props.controller.search.select(props.record, { background: true })
+        props.onSearchSelect(props.record, { background: true })
       }}
       }}
     >
     >
       <HomeSessionLeadingController
       <HomeSessionLeadingController
-        controller={props.controller}
+        server={props.server}
+        isOpenTab={props.isOpenTab}
         record={props.record}
         record={props.record}
         revealProjectOnHover={!!showProjectName()}
         revealProjectOnHover={!!showProjectName()}
       />
       />
@@ -342,12 +380,12 @@ function HomeSessionSearchResultRow(props: {
 function HomeSessionGroupHeader(props: {
 function HomeSessionGroupHeader(props: {
   title: string
   title: string
   titleOpacity: number
   titleOpacity: number
-  ref: ComponentProps<"div">["ref"]
+  onSetRef: (element: HTMLDivElement) => void
   elevated?: boolean
   elevated?: boolean
 }) {
 }) {
   return (
   return (
     <div
     <div
-      ref={props.ref}
+      ref={props.onSetRef}
       class={`pointer-events-none sticky top-[84px] lg:top-[108px] flex h-7 min-w-0 items-center justify-between pl-3 bg-v2-background-bg-base ${props.elevated ? "home-session-group-header z-[5]" : "z-10"}`}
       class={`pointer-events-none sticky top-[84px] lg:top-[108px] flex h-7 min-w-0 items-center justify-between pl-3 bg-v2-background-bg-base ${props.elevated ? "home-session-group-header z-[5]" : "z-10"}`}
     >
     >
       <div class={HOME_SECTION_LABEL} style={{ opacity: props.titleOpacity }}>
       <div class={HOME_SECTION_LABEL} style={{ opacity: props.titleOpacity }}>
@@ -357,9 +395,9 @@ function HomeSessionGroupHeader(props: {
   )
   )
 }
 }
 
 
-function HomeSessionRow(props: { controller: HomeSessionsController; record: HomeSessionRecord }) {
+function HomeSessionRow(props: HomeSessionsViewProps & { record: HomeSessionRecord }) {
   const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
   const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
-  const showProjectName = () => props.controller.showProjectName() && props.record.projectName
+  const showProjectName = () => props.showProjectName() && props.record.projectName
 
 
   return (
   return (
     <div
     <div
@@ -373,15 +411,16 @@ function HomeSessionRow(props: { controller: HomeSessionsController; record: Hom
         onMouseDown={(event) => {
         onMouseDown={(event) => {
           if (event.button === 1) event.preventDefault()
           if (event.button === 1) event.preventDefault()
         }}
         }}
-        onClick={(event) => props.controller.openSession(props.record.session, { background: isBackgroundOpen(event) })}
+        onClick={(event) => props.onOpenSession(props.record.session, { background: isBackgroundOpen(event) })}
         onAuxClick={(event) => {
         onAuxClick={(event) => {
           if (!isBackgroundOpen(event)) return
           if (!isBackgroundOpen(event)) return
           event.preventDefault()
           event.preventDefault()
-          props.controller.openSession(props.record.session, { background: true })
+          props.onOpenSession(props.record.session, { background: true })
         }}
         }}
       >
       >
         <HomeSessionLeadingController
         <HomeSessionLeadingController
-          controller={props.controller}
+          server={props.server}
+          isOpenTab={props.isOpenTab}
           record={props.record}
           record={props.record}
           revealProjectOnHover={!!showProjectName()}
           revealProjectOnHover={!!showProjectName()}
         />
         />
@@ -398,21 +437,17 @@ function HomeSessionRow(props: { controller: HomeSessionsController; record: Hom
       </button>
       </button>
       <Show when={SHOW_HOME_SESSION_ARCHIVE}>
       <Show when={SHOW_HOME_SESSION_ARCHIVE}>
         <div class="hover-reveal absolute right-1.5 top-1/2 flex -translate-y-1/2 items-center gap-1 group-hover/session:opacity-100 focus-within:opacity-100">
         <div class="hover-reveal absolute right-1.5 top-1/2 flex -translate-y-1/2 items-center gap-1 group-hover/session:opacity-100 focus-within:opacity-100">
-          <TooltipV2
-            class="flex shrink-0 items-center"
-            placement="bottom"
-            value={props.controller.language.t("common.archive")}
-          >
+          <TooltipV2 class="flex shrink-0 items-center" placement="bottom" value={props.language.t("common.archive")}>
             <IconButtonV2
             <IconButtonV2
               data-action="home-session-archive"
               data-action="home-session-archive"
               variant="ghost-muted"
               variant="ghost-muted"
               size="large"
               size="large"
               icon={<IconV2 name="archive" />}
               icon={<IconV2 name="archive" />}
-              aria-label={props.controller.language.t("common.archive")}
+              aria-label={props.language.t("common.archive")}
               onClick={(event) => {
               onClick={(event) => {
                 event.preventDefault()
                 event.preventDefault()
                 event.stopPropagation()
                 event.stopPropagation()
-                void props.controller.archiveSession(props.record.session)
+                void props.onArchiveSession(props.record.session)
               }}
               }}
             />
             />
           </TooltipV2>
           </TooltipV2>