import { useEffect, useState } from "react"; import * as Api from "./api"; import { RepositoryPicker } from "./repository-picker"; import type { ComponentType, FormEvent, ReactNode } from "react"; export type HostedWorkspaceProps = { room: string; handle: string; label: string; repository: Api.Repository; canEdit: boolean; agent?: boolean; onResetAgent?: () => Promise; }; export type HostedRoute = | { page: "repositories" } | { page: "repository"; owner: string; repository: 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 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" }; } function Frame( { children, currentRepository, openRepositoryPicker = false, user, }: { children: ReactNode; currentRepository?: Api.Repository | { owner: string; name: string; fullName: string }; openRepositoryPicker?: boolean; user: Api.User; }, ) { async function signOut() { await Api.logout(); location.assign("/"); } return (
chopin
{children}
); } function Loading({ label = "Loading" }: { label?: string }) { return (
{label}
); } function Failure({ error }: { error: unknown }) { let message = error instanceof Error ? error.message : "Something went wrong"; return (

Cannot open Chopin

{message}

Back to repositories
); } export function HostedLogin() { 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
); } function RepositoryHome({ user }: { user: Api.User }) { return (

Choose a repository from the menu to see its planning channels.

); } function RepositoryChannels( { owner, repository, user }: { owner: string; repository: string; user: Api.User }, ) { let [page, setPage] = useState(); let [error, setError] = useState(); let [title, setTitle] = useState(""); let [creating, setCreating] = useState(false); let [loadingMore, setLoadingMore] = useState(false); useEffect(() => { let active = true; Api.channels(owner, repository).then(value => { if (active) setPage(value); }, reason => { if (active) setError(reason); }); return () => { active = false; }; }, [owner, repository]); async function create(event: FormEvent) { event.preventDefault(); if (!title.trim() || creating) return; setCreating(true); try { let result = await Api.createChannel(owner, repository, title.trim()); location.assign(`/channels/${result.channel.id}`); } catch (reason) { setError(reason); setCreating(false); } } async function more() { if (!page?.nextCursor || loadingMore) return; setLoadingMore(true); try { let next = await Api.channels(owner, repository, page.nextCursor); setPage({ ...next, channels: [...page.channels, ...next.channels] }); } catch (reason) { setError(reason); } finally { setLoadingMore(false); } } if (error) return ; return (

Planning channels

One shared plan and conversation per channel.

{page?.canEdit && (
setTitle(event.target.value)} placeholder="Plan the next release" value={title} />
)} {!page ?

Loading channels...

: page.channels.length === 0 ? (

No planning channels yet

{page.canEdit ? "Create the first one above." : "A repository editor can create one."}

) : ( )} {page?.nextCursor && ( )}
); } function ChannelWorkspace( { agent, id, user, Workspace }: { agent: boolean; id: string; user: Api.User; Workspace: ComponentType; }, ) { let [detail, setDetail] = useState(); let [error, setError] = useState(); useEffect(() => { let active = true; Api.channel(id).then(value => { if (active) setDetail(value); }, reason => { if (active) setError(reason); }); return () => { active = false; }; }, [id]); if (error) return ; if (!detail) return ; return ( Api.resetAgent(id) : undefined} repository={detail.repository} room={detail.channel.id} /> ); } export function HostedApp( { agent, user, Workspace, }: { agent: boolean; user: Api.User; Workspace: ComponentType }, ) { let route = hostedRoute(location.pathname); switch (route.page) { case "repositories": return ; case "repository": return ; case "channel": return ; case "missing": return ; } } export { Failure as HostedFailure, Loading as HostedLoading };