diff --git a/packages/app/src/pages/layout-new.tsx b/packages/app/src/pages/layout-new.tsx index 6d76ca840178..20483025127a 100644 --- a/packages/app/src/pages/layout-new.tsx +++ b/packages/app/src/pages/layout-new.tsx @@ -1,14 +1,29 @@ import { createEffect, Suspense, type ParentProps } from "solid-js" import { createStore } from "solid-js/store" +import { useNavigate } from "@solidjs/router" import { DebugBar } from "@/components/debug-bar" import { TabsInfoPopup } from "@/components/help-button" import { Titlebar, type TitlebarUpdate } from "@/components/titlebar" import { usePlatform } from "@/context/platform" +import { useServer } from "@/context/server" import { setV2Toast, ToastRegion } from "@/utils/toast" +import { useOpenProject } from "@/pages/layout/open-project" +import { useDeepLinks } from "@/pages/layout/use-deep-links" export default function NewLayout(props: ParentProps) { const platform = usePlatform() + const server = useServer() + const navigate = useNavigate() const [state, setState] = createStore({ debugTools: true }) + const openProject = useOpenProject({ + open: (directory) => server.projects.open(directory), + touch: (directory) => server.projects.touch(directory), + }) + useDeepLinks({ + enabled: () => server.isLocal(), + openProject: openProject.ensureProject, + navigate, + }) createEffect(() => setV2Toast(true)) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 95f8c01843fd..f13a4fbe8ac1 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -67,13 +67,9 @@ import { latestRootSession, sortedRootSessions, } from "./layout/helpers" -import { - collectNewSessionDeepLinks, - collectOpenProjectDeepLinks, - deepLinkEvent, - drainPendingDeepLinks, -} from "./layout/deep-links" import { createInlineEditorController } from "./layout/inline-editor" +import { useOpenProject } from "./layout/open-project" +import { useDeepLinks } from "./layout/use-deep-links" import { LocalWorkspace, SortableWorkspace, @@ -118,6 +114,10 @@ export default function LegacyLayout(props: ParentProps) { const navigate = useNavigate() const providers = useProviders(() => undefined) const dialog = useDialog() + const openProjects = useOpenProject({ + projectDirectory: (directory) => projectRoot(directory), + open: (directory) => layout.projects.open(directory), + }) const command = useCommand() const theme = useTheme() const language = useLanguage() @@ -1255,41 +1255,21 @@ export default function LegacyLayout(props: ParentProps) { navigateWithSidebarReset(`/${base64Encode(session.directory)}/session/${session.id}`) } - function openProject(directory: string, navigate = true) { - layout.projects.open(directory) + async function openProject(directory: string, navigate = true) { + await openProjects.ensureProject(directory) if (navigate) return navigateToProject(directory) } - const handleDeepLinks = (urls: string[]) => { - if (!server.isLocal()) return - - for (const directory of collectOpenProjectDeepLinks(urls)) { - void openProject(directory) - } - - for (const link of collectNewSessionDeepLinks(urls)) { - void openProject(link.directory, false) - const slug = base64Encode(link.directory) - if (link.prompt) { - setSessionHandoff(SessionStateKey.from(server.scope(), SessionRouteKey.fromLegacy(slug)), { - prompt: link.prompt, - }) - } - const href = link.prompt ? `/${slug}/session?prompt=${encodeURIComponent(link.prompt)}` : `/${slug}/session` - navigateWithSidebarReset(href) - } - } - - onMount(() => { - const handler = (event: Event) => { - const detail = (event as CustomEvent<{ urls: string[] }>).detail - const urls = detail?.urls ?? [] - if (urls.length === 0) return - handleDeepLinks(urls) - } - - handleDeepLinks(drainPendingDeepLinks(window)) - makeEventListener(window, deepLinkEvent, handler as EventListener) + useDeepLinks({ + enabled: () => server.isLocal() && !settings.general.newLayoutDesigns(), + openProject: (directory) => openProjects.ensureProject(directory), + prepareNewSession: (link) => { + if (!link.prompt) return + setSessionHandoff(SessionStateKey.from(server.scope(), SessionRouteKey.fromLegacy(base64Encode(link.directory))), { + prompt: link.prompt, + }) + }, + navigate: navigateWithSidebarReset, }) async function renameProject(project: LocalProject, next: string) { diff --git a/packages/app/src/pages/layout/open-project.ts b/packages/app/src/pages/layout/open-project.ts new file mode 100644 index 000000000000..c04c3ea8380f --- /dev/null +++ b/packages/app/src/pages/layout/open-project.ts @@ -0,0 +1,25 @@ +import { useServerSDK } from "@/context/server-sdk" +import { useServerSync } from "@/context/server-sync" + +export function useOpenProject(options: { + projectDirectory?: (directory: string) => string + open: (directory: string) => void + touch?: (directory: string) => void +}) { + const serverSDK = useServerSDK() + const serverSync = useServerSync() + + async function ensureProject(directory: string) { + const projectDirectory = options.projectDirectory?.(directory) ?? directory + const known = serverSync().data.project.some((project) => project.worktree === projectDirectory) + if (!known) { + await serverSDK() + .api.project.current({ location: { directory: projectDirectory } }) + .catch(() => undefined) + } + options.open(directory) + options.touch?.(directory) + } + + return { ensureProject } +} diff --git a/packages/app/src/pages/layout/use-deep-links.ts b/packages/app/src/pages/layout/use-deep-links.ts new file mode 100644 index 000000000000..7a81c85f5943 --- /dev/null +++ b/packages/app/src/pages/layout/use-deep-links.ts @@ -0,0 +1,48 @@ +import { base64Encode } from "@opencode-ai/core/util/encode" +import { makeEventListener } from "@solid-primitives/event-listener" +import { onMount } from "solid-js" +import { + collectNewSessionDeepLinks, + collectOpenProjectDeepLinks, + deepLinkEvent, + drainPendingDeepLinks, +} from "./deep-links" + +export function useDeepLinks(options: { + enabled: () => boolean + openProject: (directory: string) => Promise + navigate: (href: string) => void + prepareNewSession?: (link: { directory: string; prompt?: string }) => void +}) { + function handleDeepLinks(urls: string[]) { + if (!options.enabled()) return + + for (const directory of collectOpenProjectDeepLinks(urls)) { + void options.openProject(directory).then(() => { + options.navigate(`/${base64Encode(directory)}/session`) + }) + } + + for (const link of collectNewSessionDeepLinks(urls)) { + void options.openProject(link.directory).then(() => { + options.prepareNewSession?.(link) + const slug = base64Encode(link.directory) + const href = link.prompt + ? `/${slug}/session?prompt=${encodeURIComponent(link.prompt)}` + : `/${slug}/session` + options.navigate(href) + }) + } + } + + onMount(() => { + const handler = (event: Event) => { + const detail = (event as CustomEvent<{ urls: string[] }>).detail + handleDeepLinks(detail?.urls ?? []) + } + handleDeepLinks(drainPendingDeepLinks(window)) + makeEventListener(window, deepLinkEvent, handler as EventListener) + }) + + return { handleDeepLinks } +}