import { useEffect, useState } from "react"; import { documentPath, documentsPath, parseDocumentPath } from "@chopin/protocol/document-url"; import * as Api from "./api"; import { readChannelRecovery, readDocumentRecovery, rememberChannel } from "./channel-recovery"; import { NavigationShell, useNavigationDocument } from "./navigation-shell"; import type { ComponentType } from "react"; export type HostedWorkspaceProps = { room: string; handle: string; label: string; slug: string; updatedAt: string; repository: Api.Repository; canEdit: boolean; agent?: boolean; userId: string; }; export type HostedRoute = | { page: "repositories" } | { page: "repository"; owner: string; repository: string } | { page: "document"; owner: string; repository: string; slug: string } | { page: "channel"; id: string } | { page: "missing" }; function decoded(value: string): string | undefined { try { return decodeURIComponent(value); } catch { return undefined; } } export function hostedRoute(pathname: string): HostedRoute { if (pathname === "/" || pathname === "") return { page: "repositories" }; let document = parseDocumentPath(pathname); if (document?.slug) { return { page: "document", owner: document.owner, repository: document.repository, slug: document.slug, }; } if (document) return { page: "repository", ...document }; let repository = /^\/repositories\/([^/]+)\/([^/]+)\/?$/.exec(pathname); if (repository) { let owner = decoded(repository[1]!); let name = decoded(repository[2]!); if (owner && name) return { page: "repository", owner, repository: name }; } let channel = /^\/channels\/([0-9a-f-]{36})\/?$/i.exec(pathname); if (channel) return { page: "channel", id: channel[1]!.toLowerCase() }; return { page: "missing" }; } export function retryableChannelFailure(error: unknown): boolean { return !(error instanceof Api.ApiError) || error.status === 408 || error.status === 429 || error.status >= 500; } function Loading({ label = "Loading" }: { label?: string }) { return (
{label}
); } function repositoryHref(repository: { owner: string; name: string }): string { return documentsPath(repository.owner, repository.name); } function canonicalize(pathname: string): void { if (location.pathname === pathname) return; history.replaceState(null, "", `${pathname}${location.search}${location.hash}`); } function Failure( { channel, error, onRetry, repository, }: { channel?: { title?: string; slug?: string }; error: unknown; onRetry?: () => void; repository?: Pick; }, ) { let message = error instanceof Error ? error.message : "Something went wrong"; return (

Cannot open Chopin

{message}

{channel && (
{channel.title &&

{channel.title}

} {channel.slug && (

{channel.slug}

)} {repository && (

{repository.fullName}

)}
)}
{onRetry && ( )} {repository && ( View {repository.fullName} channels )} Back to repositories
); } export function HostedLogin() { let href = githubLoginHref(location.pathname, location.search, location.hash); return (

chopin

Plan together, in the context of the code.

A shared document, a visible conversation, and an agent that can read the repository without owning the decision.

Open your workspace

Sign in with GitHub to choose a repository and its planning channels.

Continue with GitHub
); } export function githubLoginHref(pathname: string, search = "", hash = ""): string { let parameters = new URLSearchParams({ return_to: `${pathname}${search}${hash}` }); return `/auth/github?${parameters}`; } function ChannelWorkspace( { agent, id, owner, repository, slug, user }: { agent: boolean; id?: string; owner?: string; repository?: string; slug?: string; user: Api.User; }, ) { let [loaded, setLoaded] = useState<{ detail: Api.ChannelDetail; Workspace: ComponentType; }>(); let [error, setError] = useState(); let [retry, setRetry] = useState(0); let { channel: navigationChannel, onDocumentLoaded } = useNavigationDocument(); let recovery = id ? readChannelRecovery(user.id, id) : owner && repository && slug ? readDocumentRecovery(user.id, owner, repository, slug) : undefined; useEffect(() => { let active = true; let detail = id ? Api.channel(id) : Api.document(owner!, repository!, slug!); let prepared = detail.then(value => { if (active) { canonicalize(documentPath( value.repository.owner, value.repository.name, value.channel.slug, )); rememberChannel(user.id, value.channel, value.repository); onDocumentLoaded(value.channel); } return value; }); Promise.all([prepared, import("./room-workspace")]).then(([detail, module]) => { if (active) setLoaded({ detail, Workspace: module.RoomWorkspace }); }, reason => { if (active) setError(reason); }); return () => { active = false; }; }, [id, onDocumentLoaded, owner, repository, retry, slug, user.id]); if (error) { let requestedRepository = owner && repository ? { owner, name: repository, fullName: `${owner}/${repository}` } : undefined; return ( { setError(undefined); setRetry(value => value + 1); } : undefined} repository={recovery?.repository ?? requestedRepository} /> ); } if (!loaded) return ; let { detail, Workspace } = loaded; let channel = navigationChannel?.id === detail.channel.id ? navigationChannel : detail.channel; return ( ); } export function HostedApp( { agent, user }: { agent: boolean; user: Api.User }, ) { let route = hostedRoute(location.pathname); switch (route.page) { case "repositories": case "repository": return ; case "document": return ( ); case "channel": return ( ); case "missing": return ; } } export { Failure as HostedFailure, Loading as HostedLoading };