From bf698b3ff21910c76a02eec3b5184f6e00ba4b15 Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 16 Jun 2026 21:31:38 +0000 Subject: [PATCH] feat(web/ui): unify session opening and add repo card in project list - Add openSession utility with tab deduplication/focus - Use openSession in navbar live sessions, use-instance-actions, and project tool links - Pass onAddRepository to ProjectListItem and add dashed 'Add Repository' card - Style add-repo card in projects.css --- apps/web/src/components/app-shell.tsx | 23 ++------- .../features/project/ProjectListItem.tsx | 51 +++++++++---------- apps/web/src/hooks/use-instance-actions.ts | 23 ++------- apps/web/src/pages/ProjectsPage.tsx | 4 ++ apps/web/src/styles/pages/projects.css | 24 ++++++++- apps/web/src/utils/open-session.ts | 24 +++++++++ 6 files changed, 82 insertions(+), 67 deletions(-) create mode 100644 apps/web/src/utils/open-session.ts diff --git a/apps/web/src/components/app-shell.tsx b/apps/web/src/components/app-shell.tsx index 6664a02..54afad1 100644 --- a/apps/web/src/components/app-shell.tsx +++ b/apps/web/src/components/app-shell.tsx @@ -2,6 +2,7 @@ import { Link, NavLink, Outlet, useLocation } from "react-router-dom"; import type { Session } from "../api/sessions"; import { useTheme } from "../hooks/use-theme"; +import { openSession } from "../utils/open-session"; import { useAuth } from "../state/auth"; import { useSessions } from "../state/sessions"; import { useMobileViewport } from "../hooks/use-mobile-viewport"; @@ -33,19 +34,6 @@ const NAV_ITEMS: { const SessionItem = ({ session }: { session: Session }) => { const isRunning = session.status === "running"; - // Determine the link target: - // - Web tools open their tunnel URL - // - Terminal tools open the terminal page - // - Everything else falls back to the project page - const hasTerminal = session.tool_type_interfaces.includes("terminal"); - const hasWeb = session.tool_type_interfaces.includes("web"); - const href = - session.url && hasWeb - ? session.url - : hasTerminal - ? `/instances/${session.id}/terminal` - : `/projects/${session.project_id}`; - const contextName = session.workspace_name || session.repository_name; const tooltipParts = [ session.display_name, @@ -56,10 +44,9 @@ const SessionItem = ({ session }: { session: Session }) => { tooltipParts.push(`(${session.status})`); return ( - openSession(session)} className="nav-item session-item" title={tooltipParts.join(" · ")} > @@ -76,7 +63,7 @@ const SessionItem = ({ session }: { session: Session }) => { {session.tool_type_name} · {session.project_name} - + ); }; diff --git a/apps/web/src/components/features/project/ProjectListItem.tsx b/apps/web/src/components/features/project/ProjectListItem.tsx index ca94413..496901c 100644 --- a/apps/web/src/components/features/project/ProjectListItem.tsx +++ b/apps/web/src/components/features/project/ProjectListItem.tsx @@ -5,6 +5,7 @@ import { Icon } from "../../icon"; import type { Session } from "../../../api/sessions"; import type { IconName } from "../../../utils/icons"; import type { ProjectWithRepos, RepositorySummary } from "../../../types"; +import { openSession } from "../../../utils/open-session"; interface ProjectListItemProps { project: ProjectWithRepos; @@ -13,6 +14,7 @@ interface ProjectListItemProps { onDelete: () => void; onConfirmDelete: () => void; onCancelDelete: () => void; + onAddRepository?: () => void; sessions: Session[]; } @@ -23,6 +25,7 @@ export const ProjectListItem = ({ onDelete, onConfirmDelete, onCancelDelete, + onAddRepository, sessions, }: ProjectListItemProps) => { return ( @@ -75,17 +78,23 @@ export const ProjectListItem = ({
- {project.repositories.length === 0 ? ( -

No repositories.

- ) : ( - project.repositories.map((repo) => ( - - )) + {project.repositories.map((repo) => ( + + ))} + {onAddRepository && ( + )}
@@ -169,22 +178,10 @@ function ProjectRepoItem({ } function SessionToolLink({ session }: { session: Session }) { - const hasTerminal = session.tool_type_interfaces.includes("terminal"); - const hasWeb = session.tool_type_interfaces.includes("web"); - const href = - session.url && hasWeb - ? session.url - : hasTerminal - ? `/instances/${session.id}/terminal` - : `/projects/${session.project_id}`; - - const isExternal = href.startsWith("http"); - return ( - openSession(session)} className="workspace-tool-link" title={`${session.display_name} (${session.status})`} > @@ -195,6 +192,6 @@ function SessionToolLink({ session }: { session: Session }) { {session.display_name || session.tool_type_name} - + ); } diff --git a/apps/web/src/hooks/use-instance-actions.ts b/apps/web/src/hooks/use-instance-actions.ts index ce2dc61..ac650f6 100644 --- a/apps/web/src/hooks/use-instance-actions.ts +++ b/apps/web/src/hooks/use-instance-actions.ts @@ -1,4 +1,4 @@ -import { useState, useCallback, useRef } from "react"; +import { useState, useCallback } from "react"; import { stopInstance, deleteInstance, @@ -8,6 +8,7 @@ import { } from "../api/sessions"; import type { Session } from "../api/sessions"; import { useSessions } from "../state/sessions"; +import { openSession } from "../utils/open-session"; interface UseInstanceActionsOptions { onRefresh: () => Promise; @@ -37,27 +38,9 @@ export function useInstanceActions( null, ); const [dirtyDeleteFiles, setDirtyDeleteFiles] = useState([]); - const tabRefs = useRef>(new Map()); const handleOpen = useCallback((session: Session) => { - const key = session.id; - const existing = tabRefs.current.get(key); - if (existing && !existing.closed) { - existing.focus(); - return; - } - - let url: string; - if (session.url) { - url = session.url; - } else if (session.tool_type_interfaces?.includes("terminal")) { - url = `/instances/${session.id}/terminal`; - } else { - url = `/projects/${session.project_id}`; - } - - const w = window.open(url, `session-${session.id}`); - tabRefs.current.set(key, w); + openSession(session); }, []); const handleStart = useCallback( diff --git a/apps/web/src/pages/ProjectsPage.tsx b/apps/web/src/pages/ProjectsPage.tsx index 2c529ab..8e28144 100644 --- a/apps/web/src/pages/ProjectsPage.tsx +++ b/apps/web/src/pages/ProjectsPage.tsx @@ -252,6 +252,10 @@ export const ProjectsPage = () => { onDelete={() => setDeleteConfirmId(project.id)} onConfirmDelete={() => void handleDelete(project.id)} onCancelDelete={() => setDeleteConfirmId(null)} + onAddRepository={() => { + setSelectedProject(project); + setMobileView("create-repo"); + }} /> ))} diff --git a/apps/web/src/styles/pages/projects.css b/apps/web/src/styles/pages/projects.css index d0cb59c..0935808 100644 --- a/apps/web/src/styles/pages/projects.css +++ b/apps/web/src/styles/pages/projects.css @@ -177,8 +177,28 @@ color: var(--danger); } -.project-add-repo { - display: none; +.project-add-repo-card { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-3); + background: transparent; + color: var(--muted); + border-style: dashed; + cursor: pointer; + min-height: 120px; +} + +.project-add-repo-card:hover { + background: var(--bg); + color: var(--ink); + border-color: var(--brand); +} + +.project-add-repo-card .project-repo-name { + font-weight: 500; + white-space: normal; + text-align: center; } @media (max-width: 767px) { diff --git a/apps/web/src/utils/open-session.ts b/apps/web/src/utils/open-session.ts new file mode 100644 index 0000000..e6148e1 --- /dev/null +++ b/apps/web/src/utils/open-session.ts @@ -0,0 +1,24 @@ +import type { Session } from "../api/sessions"; + +const tabRefs = new Map(); + +export function openSession(session: Session): void { + const key = session.id; + const existing = tabRefs.get(key); + if (existing && !existing.closed) { + existing.focus(); + return; + } + + let url: string; + if (session.url) { + url = session.url; + } else if (session.tool_type_interfaces?.includes("terminal")) { + url = `/instances/${session.id}/terminal`; + } else { + url = `/projects/${session.project_id}`; + } + + const w = window.open(url, `session-${session.id}`); + tabRefs.set(key, w); +}