diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 7112018371..6b5a38ab7e 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -1,65 +1,8 @@ -import { type ComponentProps, createMemo, For, Show } from "solid-js" -import { DragDropProvider, PointerSensor } from "@dnd-kit/solid" -import { isSortable, useSortable } from "@dnd-kit/solid/sortable" -import { AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom" -import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers" -import { RestrictToElement } from "@dnd-kit/dom/modifiers" -import { Spinner } from "@opencode-ai/ui/spinner" import { ScrollView } from "@opencode-ai/ui/scroll-view" -import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2" -import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" -import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" -import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" -import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" -import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" -import { getProjectAvatarVariant, type LocalProject } from "@/context/layout" -import { ServerConnection } from "@/context/server" -import { useLanguage } from "@/context/language" -import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers" -import { SessionTabAvatarView } from "@/pages/layout/session-tab-avatar" -import { sessionTitle } from "@/utils/session-title" -import { ServerRowMenuView, serverMenuLabels } from "@/components/server/server-row-menu" -import { ServerHealthIndicator } from "@/components/server/server-row" -import { type ServerHealth } from "@/utils/server-health" -import { shouldOpenSessionInBackground } from "./home-session-open" -import { createHomeProjectsController, type HomeProjectsController } from "./home/home-projects-controller" -import { - createHomeSessionsController, - HomeSessionStatusController, - homeSessionSearchKey, - type HomeSessionRecord, - type HomeSessionsController, -} from "./home/home-sessions-controller" - -const SHOW_HOME_SESSION_ARCHIVE = false -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" -const HOME_ROW_BASE = `${HOME_ROW_LAYOUT} border-0` -const HOME_ROW = `${HOME_ROW_BASE} [font-weight:530] text-v2-text-text-muted hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover` -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_SECTION_LABEL = "text-v2-text-text-muted [font-weight:440]" - -const HOME_SESSION_SEARCH_RESULTS_ID = "home-session-search-results" -const HOME_SEARCH_RESULT_ROW = - "flex h-10 w-full shrink-0 cursor-default items-center gap-2 border-0 py-3 pl-[18px] pr-6 text-left transition-[background-color] duration-[120ms] ease-in-out hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none" -const HOME_SEARCH_RESULT_TITLE = - "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-base [font-weight:530]" -const HOME_SEARCH_RESULT_META = - "min-w-0 flex-[1_1_auto] overflow-hidden text-ellipsis whitespace-nowrap text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-muted [font-weight:440]" - -// Middle-click or Cmd+click on macOS (Ctrl+click elsewhere) opens a session -// tab in the background without navigating, matching browser conventions. -function isBackgroundOpen(event: MouseEvent) { - return shouldOpenSessionInBackground({ - button: event.button, - mac: typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform), - meta: event.metaKey, - ctrl: event.ctrlKey, - shift: event.shiftKey, - alt: event.altKey, - }) -} +import { createHomeProjectsController } from "./home/home-projects-controller" +import { HomeProjectsView, HomeUtilityNav } from "./home/home-projects-view" +import { createHomeSessionsController } from "./home/home-sessions-controller" +import { HomeSessionsView } from "./home/home-sessions-view" export function NewHome() { const projects = createHomeProjectsController() @@ -75,7 +18,7 @@ export function NewHome() { onWheel={sessions.scroll.containOuterWheel} >
- + ) } - -function HomeSessionsView(props: { controller: HomeSessionsController }) { - return ( -
-
- - 0 && props.controller.canCreateSession()}> -
- - {props.controller.language.t("command.session.new")} - -
-
-
- -
- ) -} - -function HomeProjectColumn(props: { controller: HomeProjectsController; onWheel: (event: WheelEvent) => void }) { - return ( - - ) -} - -function HomeUtilityNav(props: { - class?: string - openSettings: () => void - openHelp: () => void - language: ReturnType -}) { - return ( -
- - -
- ) -} - -function HomeServerRow(props: { - controller: HomeProjectsController - server: ServerConnection.Any - selected: boolean - collapsed: boolean - health: ServerHealth | undefined -}) { - const healthy = () => !!props.health?.healthy - const canToggle = () => healthy() && props.controller.projectsForServer(props.server).length > 0 - const menuID = () => props.controller.serverMenuID(props.server) - return ( -
- -
- 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)} - /> - - } - aria-label={props.controller.language.t("home.project.add")} - disabled={props.health?.healthy === false} - onClick={() => props.controller.chooseProject(props.server)} - /> - -
-
- ) -} - -type HomeProjectListProps = { - controller: HomeProjectsController - server: ServerConnection.Any - projects: LocalProject[] -} - -function HomeProjectList(props: HomeProjectListProps) { - let listRef!: HTMLDivElement - - return ( - [ - ...defaults.filter((sensor) => sensor !== PointerSensor), - PointerSensor.configure({ - activationConstraints: (event) => - event.pointerType === "touch" - ? [new PointerActivationConstraints.Delay({ value: 250, tolerance: 5 })] - : [new PointerActivationConstraints.Distance({ value: 4 })], - preventActivation: (event) => event.target instanceof Element && !!event.target.closest("[data-action]"), - }), - ]} - modifiers={[RestrictToVerticalAxis, RestrictToElement.configure({ element: () => listRef })]} - plugins={(defaults) => [ - ...defaults.filter((plugin) => plugin !== AutoScroller && plugin !== Feedback), - AutoScroller.configure({ acceleration: 8, threshold: { x: 0, y: 0.05 } }), - Feedback.configure({ dropAnimation: null }), - ]} - onDragEnd={(event) => { - const source = event.operation.source - 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()) - }} - > -
- {/* Keyed on worktree strings: the enriched project objects are - recreated on every store or sync update, so iterating them directly - remounts all rows — killing any in-flight drag activation (the - row's sortable unregisters on unmount) and discarding animations. - String keys keep row elements alive and move them on reorder. */} - project.worktree)}> - {(worktree, index) => } - -
-
- ) -} - -function HomeProjectSlot( - props: HomeProjectListProps & { - worktree: string - index: () => number - }, -) { - const project = createMemo(() => props.projects.find((item) => item.worktree === props.worktree)) - - return ( - - {(item) => ( - - )} - - ) -} - -function HomeProjectEmpty(props: { - controller: HomeProjectsController - server: ServerConnection.Any - recentlyClosed: LocalProject[] -}) { - const unreachable = () => props.controller.serverHealth(props.server)?.healthy === false - return ( -
- - 0}> -
-
- {props.controller.language.t("home.recentlyClosed")} -
-
- - {(project) => } - -
-
- ) -} - -function HomeRecentlyClosedRow(props: { - controller: HomeProjectsController - project: LocalProject - server: ServerConnection.Any -}) { - const unreachable = () => props.controller.serverHealth(props.server)?.healthy === false - const path = () => { - const home = props.controller.homedir() - const worktree = props.project.worktree - if (home && (worktree === home || worktree.startsWith(`${home}/`))) return `~${worktree.slice(home.length)}` - return worktree - } - return ( - - - - ) -} - -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 - const sortable = useSortable({ - get id() { - return props.project.worktree - }, - get index() { - return props.index() - }, - }) - let pointerDownSelected: boolean | undefined - const menuID = () => props.controller.projectMenuID(props.server, props.project.worktree) - return ( -
- -
- props.controller.setMenuOpen(menuID(), open)} - > - } - aria-label={props.controller.language.t("common.moreOptions")} - /> - - - props.controller.openProjectNewSession(props.server, props.project.worktree)} - > - {props.controller.language.t("command.session.new")} - - props.controller.editProject(props.server, props.project)}> - {props.controller.language.t("dialog.project.edit.title")} - - - props.controller.revealProject(props.server, props.project)}> - {props.controller.fileManagerActionLabel()} - - - props.controller.clearNotifications(props.server, props.project)} - > - {props.controller.language.t("sidebar.project.clearNotifications")} - - - props.controller.closeProject(props.server, props.project.worktree)}> - {props.controller.language.t("common.close")} - - - - - } - aria-label={props.controller.language.t("command.session.new")} - onClick={() => props.controller.openProjectNewSession(props.server, props.project.worktree)} - /> -
-
- ) -} - -function HomeProjectAvatar(props: { project: LocalProject; outline?: boolean }) { - const name = createMemo(() => displayName(props.project)) - return ( - - ) -} - -function HomeSessionLeadingController(props: { - controller: HomeSessionsController - record: HomeSessionRecord - revealProjectOnHover: boolean -}) { - return ( - ( - - )} - /> - ) -} - -function HomeSessionLeading(props: { - record: HomeSessionRecord - revealProjectOnHover: boolean - open: boolean - unread: boolean - loading: boolean -}) { - return ( -
- - - -
- ) -} - -function HomeSessionSearch(props: { controller: HomeSessionsController }) { - return ( -
-
- -
-
-
- - -
- } - > - 0} - fallback={ -

- {props.controller.search.noResultsLabel()} -

- } - > -
-

- {props.controller.language.t("home.sessions.search.sessions")} -

- -
- - {(record) => ( - - )} - -
-
-
-
- -
-
-
- - -
-
- ) -} - -function HomeSessionSearchResultRow(props: { - controller: HomeSessionsController - record: HomeSessionRecord - selected: boolean -}) { - const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) - const showProjectName = () => props.controller.showProjectName() && props.record.projectName - - const key = () => homeSessionSearchKey(props.record) - - return ( - - ) -} - -function HomeSessionGroupHeader(props: { - title: string - titleOpacity: number - ref: ComponentProps<"div">["ref"] - elevated?: boolean -}) { - return ( -
-
- {props.title} -
-
- ) -} - -function HomeSessionRow(props: { controller: HomeSessionsController; record: HomeSessionRecord }) { - const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) - const showProjectName = () => props.controller.showProjectName() && props.record.projectName - - return ( -
- - -
- - } - aria-label={props.controller.language.t("common.archive")} - onClick={(event) => { - event.preventDefault() - event.stopPropagation() - void props.controller.archiveSession(props.record.session) - }} - /> - -
-
-
- ) -} - -function HomeSessionsEmpty(props: { onNewSession?: () => void; language: ReturnType }) { - return ( -
-
- {props.language.t("home.sessions.empty")} -
-

- {props.language.t("home.sessions.empty.description")} -

- - {(onNewSession) => ( - - {props.language.t("command.session.new")} - - )} - -
- ) -} - -function HomeSessionSkeleton(props: { label: string }) { - return ( -
-
- -
- - ) -} diff --git a/packages/app/src/pages/home/home-projects-view.tsx b/packages/app/src/pages/home/home-projects-view.tsx new file mode 100644 index 0000000000..cc8be8e75f --- /dev/null +++ b/packages/app/src/pages/home/home-projects-view.tsx @@ -0,0 +1,524 @@ +import { createMemo, For, Show } from "solid-js" +import { DragDropProvider, PointerSensor } from "@dnd-kit/solid" +import { isSortable, useSortable } from "@dnd-kit/solid/sortable" +import { AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom" +import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers" +import { RestrictToElement } from "@dnd-kit/dom/modifiers" +import { ScrollView } from "@opencode-ai/ui/scroll-view" +import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { getProjectAvatarVariant, type LocalProject } from "@/context/layout" +import { ServerConnection } from "@/context/server" +import { useLanguage } from "@/context/language" +import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers" +import { ServerRowMenuView, serverMenuLabels } from "@/components/server/server-row-menu" +import { ServerHealthIndicator } from "@/components/server/server-row" +import { type ServerHealth } from "@/utils/server-health" +import { type HomeProjectsController } from "./home-projects-controller" + +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" +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)]` + +export function HomeProjectsView(props: { controller: HomeProjectsController; onWheel: (event: WheelEvent) => void }) { + return ( + + ) +} + +export function HomeUtilityNav(props: { + class?: string + openSettings: () => void + openHelp: () => void + language: ReturnType +}) { + return ( +
+ + +
+ ) +} + +function HomeServerRow(props: { + controller: HomeProjectsController + server: ServerConnection.Any + selected: boolean + collapsed: boolean + health: ServerHealth | undefined +}) { + const healthy = () => !!props.health?.healthy + const canToggle = () => healthy() && props.controller.projectsForServer(props.server).length > 0 + const menuID = () => props.controller.serverMenuID(props.server) + return ( +
+ +
+ 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)} + /> + + } + aria-label={props.controller.language.t("home.project.add")} + disabled={props.health?.healthy === false} + onClick={() => props.controller.chooseProject(props.server)} + /> + +
+
+ ) +} + +type HomeProjectListProps = { + controller: HomeProjectsController + server: ServerConnection.Any + projects: LocalProject[] +} + +function HomeProjectList(props: HomeProjectListProps) { + let listRef!: HTMLDivElement + + return ( + [ + ...defaults.filter((sensor) => sensor !== PointerSensor), + PointerSensor.configure({ + activationConstraints: (event) => + event.pointerType === "touch" + ? [new PointerActivationConstraints.Delay({ value: 250, tolerance: 5 })] + : [new PointerActivationConstraints.Distance({ value: 4 })], + preventActivation: (event) => event.target instanceof Element && !!event.target.closest("[data-action]"), + }), + ]} + modifiers={[RestrictToVerticalAxis, RestrictToElement.configure({ element: () => listRef })]} + plugins={(defaults) => [ + ...defaults.filter((plugin) => plugin !== AutoScroller && plugin !== Feedback), + AutoScroller.configure({ acceleration: 8, threshold: { x: 0, y: 0.05 } }), + Feedback.configure({ dropAnimation: null }), + ]} + onDragEnd={(event) => { + const source = event.operation.source + 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()) + }} + > +
+ {/* Keyed on worktree strings: the enriched project objects are + recreated on every store or sync update, so iterating them directly + remounts all rows — killing any in-flight drag activation (the + row's sortable unregisters on unmount) and discarding animations. + String keys keep row elements alive and move them on reorder. */} + project.worktree)}> + {(worktree, index) => } + +
+
+ ) +} + +function HomeProjectSlot( + props: HomeProjectListProps & { + worktree: string + index: () => number + }, +) { + const project = createMemo(() => props.projects.find((item) => item.worktree === props.worktree)) + + return ( + + {(item) => ( + + )} + + ) +} + +function HomeProjectEmpty(props: { + controller: HomeProjectsController + server: ServerConnection.Any + recentlyClosed: LocalProject[] +}) { + const unreachable = () => props.controller.serverHealth(props.server)?.healthy === false + return ( +
+ + 0}> +
+
+ {props.controller.language.t("home.recentlyClosed")} +
+
+ + {(project) => } + +
+
+ ) +} + +function HomeRecentlyClosedRow(props: { + controller: HomeProjectsController + project: LocalProject + server: ServerConnection.Any +}) { + const unreachable = () => props.controller.serverHealth(props.server)?.healthy === false + const path = () => { + const home = props.controller.homedir() + const worktree = props.project.worktree + if (home && (worktree === home || worktree.startsWith(`${home}/`))) return `~${worktree.slice(home.length)}` + return worktree + } + return ( + + + + ) +} + +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 + const sortable = useSortable({ + get id() { + return props.project.worktree + }, + get index() { + return props.index() + }, + }) + let pointerDownSelected: boolean | undefined + const menuID = () => props.controller.projectMenuID(props.server, props.project.worktree) + return ( +
+ +
+ props.controller.setMenuOpen(menuID(), open)} + > + } + aria-label={props.controller.language.t("common.moreOptions")} + /> + + + props.controller.openProjectNewSession(props.server, props.project.worktree)} + > + {props.controller.language.t("command.session.new")} + + props.controller.editProject(props.server, props.project)}> + {props.controller.language.t("dialog.project.edit.title")} + + + props.controller.revealProject(props.server, props.project)}> + {props.controller.fileManagerActionLabel()} + + + props.controller.clearNotifications(props.server, props.project)} + > + {props.controller.language.t("sidebar.project.clearNotifications")} + + + props.controller.closeProject(props.server, props.project.worktree)}> + {props.controller.language.t("common.close")} + + + + + } + aria-label={props.controller.language.t("command.session.new")} + onClick={() => props.controller.openProjectNewSession(props.server, props.project.worktree)} + /> +
+
+ ) +} + +function HomeProjectAvatar(props: { project: LocalProject; outline?: boolean }) { + const name = createMemo(() => displayName(props.project)) + return ( + + ) +} diff --git a/packages/app/src/pages/home/home-sessions-view.tsx b/packages/app/src/pages/home/home-sessions-view.tsx new file mode 100644 index 0000000000..12b383c4a9 --- /dev/null +++ b/packages/app/src/pages/home/home-sessions-view.tsx @@ -0,0 +1,456 @@ +import { type ComponentProps, createMemo, For, Show } from "solid-js" +import { Spinner } from "@opencode-ai/ui/spinner" +import { ScrollView } from "@opencode-ai/ui/scroll-view" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { useLanguage } from "@/context/language" +import { SessionTabAvatarView } from "@/pages/layout/session-tab-avatar" +import { sessionTitle } from "@/utils/session-title" +import { shouldOpenSessionInBackground } from "../home-session-open" +import { + HomeSessionStatusController, + homeSessionSearchKey, + type HomeSessionRecord, + type HomeSessionsController, +} from "./home-sessions-controller" + +const SHOW_HOME_SESSION_ARCHIVE = false +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" +const HOME_ROW_BASE = `${HOME_ROW_LAYOUT} border-0` +const HOME_ROW = `${HOME_ROW_BASE} [font-weight:530] text-v2-text-text-muted hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover` +const HOME_SECTION_LABEL = "text-v2-text-text-muted [font-weight:440]" +const HOME_SESSION_SEARCH_RESULTS_ID = "home-session-search-results" +const HOME_SEARCH_RESULT_ROW = + "flex h-10 w-full shrink-0 cursor-default items-center gap-2 border-0 py-3 pl-[18px] pr-6 text-left transition-[background-color] duration-[120ms] ease-in-out hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none" +const HOME_SEARCH_RESULT_TITLE = + "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-base [font-weight:530]" +const HOME_SEARCH_RESULT_META = + "min-w-0 flex-[1_1_auto] overflow-hidden text-ellipsis whitespace-nowrap text-[13px] leading-4 tracking-[-0.04px] text-v2-text-text-muted [font-weight:440]" + +// Middle-click or Cmd+click on macOS (Ctrl+click elsewhere) opens a session +// tab in the background without navigating, matching browser conventions. +function isBackgroundOpen(event: MouseEvent) { + return shouldOpenSessionInBackground({ + button: event.button, + mac: typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform), + meta: event.metaKey, + ctrl: event.ctrlKey, + shift: event.shiftKey, + alt: event.altKey, + }) +} + +export function HomeSessionsView(props: { controller: HomeSessionsController }) { + return ( +
+
+ + 0 && props.controller.canCreateSession()}> +
+ + {props.controller.language.t("command.session.new")} + +
+
+
+ +
+ ) +} + +function HomeSessionLeadingController(props: { + controller: HomeSessionsController + record: HomeSessionRecord + revealProjectOnHover: boolean +}) { + return ( + ( + + )} + /> + ) +} + +function HomeSessionLeading(props: { + record: HomeSessionRecord + revealProjectOnHover: boolean + open: boolean + unread: boolean + loading: boolean +}) { + return ( +
+ + + +
+ ) +} + +function HomeSessionSearch(props: { controller: HomeSessionsController }) { + return ( +
+
+ +
+
+
+ + +
+ } + > + 0} + fallback={ +

+ {props.controller.search.noResultsLabel()} +

+ } + > +
+

+ {props.controller.language.t("home.sessions.search.sessions")} +

+ +
+ + {(record) => ( + + )} + +
+
+
+
+ +
+
+
+ + +
+
+ ) +} + +function HomeSessionSearchResultRow(props: { + controller: HomeSessionsController + record: HomeSessionRecord + selected: boolean +}) { + const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) + const showProjectName = () => props.controller.showProjectName() && props.record.projectName + const key = () => homeSessionSearchKey(props.record) + + return ( + + ) +} + +function HomeSessionGroupHeader(props: { + title: string + titleOpacity: number + ref: ComponentProps<"div">["ref"] + elevated?: boolean +}) { + return ( +
+ +
+ ) +} + +function HomeSessionRow(props: { controller: HomeSessionsController; record: HomeSessionRecord }) { + const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) + const showProjectName = () => props.controller.showProjectName() && props.record.projectName + + return ( +
+ + +
+ + } + aria-label={props.controller.language.t("common.archive")} + onClick={(event) => { + event.preventDefault() + event.stopPropagation() + void props.controller.archiveSession(props.record.session) + }} + /> + +
+
+
+ ) +} + +function HomeSessionsEmpty(props: { onNewSession?: () => void; language: ReturnType }) { + return ( +
+
+ {props.language.t("home.sessions.empty")} +
+

+ {props.language.t("home.sessions.empty.description")} +

+ + {(onNewSession) => ( + + {props.language.t("command.session.new")} + + )} + +
+ ) +} + +function HomeSessionSkeleton(props: { label: string }) { + return ( +
+
+ +
+ + ) +}