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;
};
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 (
);
}
function Loading({ label = "Loading" }: { label?: string }) {
return (
{label}
);
}
function Failure({ error }: { error: unknown }) {
let message = error instanceof Error ? error.message : "Something went wrong";
return (
);
}
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 && (
)}
{!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 (
);
}
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 };