Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 1 | import { Archive, Hash, Lock, Search } from "lucide-react"; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 2 | import { useMemo, useState } from "react"; |
| 3 | import { Link, useNavigate } from "react-router"; | |
| 4 | ||
| 5 | import type { Route } from "./+types/browse"; | |
| 6 | import { ChatUnavailable } from "../../../components/chat/empty"; | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 7 | import { CreateChannelButton, OWNERS_ONLY_CHANNELS, useChannelCreation, useChatSend } from "../../../components/chat/actions"; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 8 | import { channelPath } from "../../../lib/chat"; |
| 9 | import { sidebarOrNull } from "../../../lib/chat.server"; | |
| 10 | import { page } from "../../../lib/meta"; | |
| 11 | import { chat } from "../../../lib/services.server"; | |
| 12 | import { requireUser } from "../../../lib/session.server"; | |
| 13 | ||
| 14 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 15 | return page(args, { title: `Browse channels · ${params.owner} · g1t` }); | |
| 16 | } | |
| 17 | ||
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 18 | /** |
| 19 | * Every public channel in the workspace, joined or not, and the private | |
| 20 | * ones the viewer is in (the chat service leaves out the rest). With | |
| 21 | * `?archived=1`, the archived ones instead. | |
| 22 | */ | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 23 | export async function loader({ params, context, request }: Route.LoaderArgs) { |
| 24 | const viewer = requireUser(context, request); | |
| 25 | const slug = params.owner.toLowerCase(); | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 26 | const archived = new URL(request.url).searchParams.get("archived") === "1"; |
| 27 | const [listed, sidebar] = await Promise.all([chat.browse(slug, viewer, { archived }).catch(() => null), sidebarOrNull(slug, viewer)]); | |
| 28 | if (!listed?.ok) return { slug, unavailable: true as const }; | |
| 29 | const joined = (sidebar?.entries ?? []).map((entry) => entry.channel.id); | |
| 30 | return { slug, unavailable: false as const, archived, channels: listed.value, joined }; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 31 | } |
| 32 | ||
| 33 | export default function Browse({ loaderData }: Route.ComponentProps) { | |
| 34 | const [query, setQuery] = useState(""); | |
| 35 | const navigate = useNavigate(); | |
| 36 | const send = useChatSend(loaderData.slug); | |
| 37 | const [joining, setJoining] = useState<string | null>(null); | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 38 | const may = useChannelCreation(); |
| Chat and workspace agents: channels, DMs and named agents you talk to | 39 | const shown = useMemo(() => { |
| 40 | if (loaderData.unavailable) return []; | |
| 41 | const q = query.trim().toLowerCase().replace(/^#/, ""); | |
| 42 | return loaderData.channels.filter((c) => !q || (c.name ?? "").includes(q) || (c.topic ?? "").toLowerCase().includes(q)); | |
| 43 | }, [loaderData, query]); | |
| 44 | if (loaderData.unavailable) return <ChatUnavailable title="Browse channels" />; | |
| 45 | const joined = new Set(loaderData.joined); | |
| 46 | return ( | |
| 47 | <div className="mx-auto w-full max-w-3xl px-4 py-8 sm:px-8 lg:py-12"> | |
| 48 | <div className="flex flex-wrap items-center justify-between gap-3"> | |
| 49 | <div> | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 50 | <h1 className="text-2xl font-semibold tracking-tight">{loaderData.archived ? "Archived channels" : "Browse channels"}</h1> |
| 51 | <p className="mt-1 text-sm text-muted"> | |
| 52 | {loaderData.archived | |
| 53 | ? "Channels no one posts in any more. Their history stays readable; whoever may archive a channel may bring it back." | |
| 54 | : `Every public channel in ${loaderData.slug}, which anyone in the workspace can read and join, and the private ones you're in.`} | |
| 55 | </p> | |
| 56 | {!loaderData.archived && !may.any && <p className="mt-1 text-xs text-faint">{OWNERS_ONLY_CHANNELS}</p>} | |
| 57 | </div> | |
| 58 | <div className="flex items-center gap-2"> | |
| 59 | <Link | |
| 60 | to={loaderData.archived ? `/${loaderData.slug}/-/chat/browse` : `/${loaderData.slug}/-/chat/browse?archived=1`} | |
| 61 | className="inline-flex h-9 items-center gap-2 rounded-md px-3 text-sm text-muted transition-colors hover:bg-surface hover:text-fg" | |
| 62 | > | |
| 63 | {loaderData.archived ? <Hash size={15} /> : <Archive size={15} />} | |
| 64 | {loaderData.archived ? "Current channels" : "Archived"} | |
| 65 | </Link> | |
| 66 | {!loaderData.archived && <CreateChannelButton slug={loaderData.slug} variant="button" />} | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 67 | </div> |
| 68 | </div> | |
| 69 | <label className="mt-6 flex h-10 items-center gap-2 rounded-lg border border-line bg-surface px-3 text-sm focus-within:border-accent-dim"> | |
| 70 | <Search size={15} className="text-faint" /> | |
| 71 | <input | |
| 72 | value={query} | |
| 73 | onChange={(event) => setQuery(event.target.value)} | |
| 74 | placeholder="Search channels" | |
| 75 | aria-label="Search channels" | |
| 76 | className="min-w-0 grow bg-transparent outline-none placeholder:text-faint" | |
| 77 | /> | |
| 78 | </label> | |
| 79 | <ul className="mt-4 divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface"> | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 80 | {shown.length === 0 && ( |
| 81 | <li className="px-4 py-10 text-center text-sm text-muted"> | |
| 82 | {query ? "No channel matches." : loaderData.archived ? "No archived channels." : "No channels yet."} | |
| 83 | </li> | |
| 84 | )} | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 85 | {shown.map((channel) => { |
| 86 | const isIn = joined.has(channel.id); | |
| 87 | return ( | |
| Merge branch 'chat-sidebar' into fast-push | 88 | // The whole row opens the channel: its name's link reaches over |
| 89 | // it (`after:inset-0`), and Join sits above that, so nothing | |
| 90 | // interactive nests inside anything else. | |
| 91 | <li | |
| 92 | key={channel.id} | |
| 93 | className="group/card relative flex items-center gap-3 px-4 py-3 transition-colors hover:bg-raised/50 has-[a:focus-visible]:bg-raised/50 has-[a:focus-visible]:ring-2 has-[a:focus-visible]:ring-accent has-[a:focus-visible]:ring-inset" | |
| 94 | > | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 95 | {channel.private ? ( |
| 96 | <Lock size={15} className="shrink-0 text-faint" aria-label="Private" /> | |
| 97 | ) : ( | |
| 98 | <Hash size={16} className="shrink-0 text-faint" aria-label="Public" /> | |
| 99 | )} | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 100 | <div className="min-w-0 grow"> |
| Merge branch 'chat-sidebar' into fast-push | 101 | <Link |
| 102 | to={channelPath(loaderData.slug, channel)} | |
| 103 | className="font-medium outline-none group-hover/card:text-accent after:absolute after:inset-0 after:content-['']" | |
| 104 | > | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 105 | {channel.name} |
| 106 | </Link> | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 107 | {channel.private && <span className="ml-2 text-xs text-faint">Private</span>} |
| Chat and workspace agents: channels, DMs and named agents you talk to | 108 | {channel.topic && <p className="truncate text-sm text-muted">{channel.topic}</p>} |
| 109 | </div> | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 110 | {loaderData.archived ? ( |
| 111 | <span className="text-xs text-faint">Archived</span> | |
| 112 | ) : isIn ? ( | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 113 | <span className="text-xs text-faint">Joined</span> |
| 114 | ) : ( | |
| 115 | <button | |
| 116 | type="button" | |
| 117 | disabled={joining === channel.id} | |
| 118 | onClick={async () => { | |
| 119 | setJoining(channel.id); | |
| 120 | const done = await send({ intent: "join", channel_id: channel.id }); | |
| 121 | setJoining(null); | |
| 122 | if (done.ok) navigate(channelPath(loaderData.slug, channel)); | |
| 123 | }} | |
| Merge branch 'chat-sidebar' into fast-push | 124 | className="relative z-10 h-8 rounded-md border border-line px-3 text-[0.8125rem] font-medium text-fg/90 transition-colors hover:border-line-strong hover:bg-raised disabled:opacity-50" |
| Chat and workspace agents: channels, DMs and named agents you talk to | 125 | > |
| 126 | {joining === channel.id ? "Joining…" : "Join"} | |
| 127 | </button> | |
| 128 | )} | |
| 129 | </li> | |
| 130 | ); | |
| 131 | })} | |
| 132 | </ul> | |
| 133 | </div> | |
| 134 | ); | |
| 135 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.