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.
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 1 | import { ArrowRight, AtSign, GitPullRequest, Inbox, MessagesSquare, Sparkles } from "lucide-react"; |
| 2 | import { type ReactNode, Suspense, useEffect, useState } from "react"; | |
| 3 | import { Await, Form, Link, data, redirect, useNavigation } from "react-router"; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 4 | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 5 | import { type ChatSidebarEntry, type InboxItem, type WorkspaceAgent, hasCodeAccess } from "@g1t/contracts"; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 6 | |
| 7 | import type { Route } from "./+types/home"; | |
| 8 | import { AgentFace } from "../../components/agents-mode"; | |
| 9 | import { MemberAvatar, StatusDot, statusLabel } from "../../components/chat/marks"; | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 10 | import { InboxCard } from "../../components/inbox"; |
| 11 | import { G1tMark, isOrchestrator } from "../../components/orchestrator"; | |
| 12 | import { Skeleton } from "../../components/ui/skeleton"; | |
| 13 | import { channelPath } from "../../lib/chat"; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 14 | import { sidebarOrNull } from "../../lib/chat.server"; |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 15 | import { greetingFor, hourIn, readCookie } from "../../lib/mission"; |
| 16 | import type { NeedRow } from "../../lib/mission-control"; | |
| 17 | import { loadMissionControl } from "../../lib/mission-control.server"; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 18 | import { page } from "../../lib/meta"; |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 19 | import { chat, inbox, workspaceAgents } from "../../lib/services.server"; |
| 20 | import { assertSameOrigin, requireUser, roleIn } from "../../lib/session.server"; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 21 | |
| 22 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 23 | return page(args, { title: `Home · ${params.owner} · g1t` }); | |
| 24 | } | |
| 25 | ||
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 26 | /** Unread inbox items read, and how many Home shows. */ |
| 27 | const INBOX_READ = 30; | |
| 28 | const INBOX_SHOWN = 4; | |
| 29 | /** Most rows in each of Home's lists. */ | |
| 30 | const ROWS = 5; | |
| 31 | ||
| Chat and workspace agents: channels, DMs and named agents you talk to | 32 | /** |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 33 | * Home: what needs you across the workspace, never one mode's sidebar. |
| 34 | * Each section is read at once and on its own, and one whose service does | |
| 35 | * not answer is left saying so. Reviews and approvals come from Code's | |
| 36 | * Overview, the slowest part, so they arrive after the page does. | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 37 | */ |
| 38 | export async function loader({ params, context, request }: Route.LoaderArgs) { | |
| 39 | const viewer = requireUser(context, request); | |
| 40 | const slug = params.owner.toLowerCase(); | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 41 | if (!roleIn(viewer, slug)) throw data(null, { status: 404 }); |
| 42 | const membership = viewer.workspaces?.find((m) => m.slug === slug) ?? null; | |
| 43 | const code = hasCodeAccess(membership); | |
| 44 | // Streamed: the page shows without waiting for it. | |
| 45 | const reviews: Promise<NeedRow[] | null> | null = code | |
| 46 | ? loadMissionControl(viewer, request, slug) | |
| 47 | .then(({ value }) => value.needs.slice(0, ROWS)) | |
| 48 | .catch(() => null) | |
| 49 | : null; | |
| 50 | const [sidebar, agents, unread] = await Promise.all([ | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 51 | sidebarOrNull(slug, viewer), |
| 52 | workspaceAgents.list(slug, viewer).catch(() => null), | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 53 | inbox.list(viewer, { unread: true, limit: INBOX_READ }).catch(() => null), |
| Chat and workspace agents: channels, DMs and named agents you talk to | 54 | ]); |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 55 | const tz = readCookie(request.headers.get("cookie"), "g1t_tz"); |
| 56 | const listed = agents?.ok ? agents.value.filter((agent) => !agent.archived_at) : null; | |
| 57 | const items: InboxItem[] | null = unread?.items ?? null; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 58 | return { |
| 59 | slug, | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 60 | code, |
| Chat and workspace agents: channels, DMs and named agents you talk to | 61 | name: viewer.username, |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 62 | greeting: greetingFor(hourIn(Date.now(), tz)), |
| 63 | chat: sidebar | |
| 64 | ? sidebar.entries | |
| 65 | .filter((entry) => !entry.muted && (entry.mentions > 0 || (entry.channel.kind === "dm" && entry.unread > 0))) | |
| 66 | .sort((a, b) => b.mentions - a.mentions || (b.channel.last_message_at ?? "").localeCompare(a.channel.last_message_at ?? "")) | |
| 67 | : null, | |
| 68 | agents: listed, | |
| 69 | orchestrator: listed?.find(isOrchestrator) ?? null, | |
| 70 | inbox: items ? [...items].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, INBOX_SHOWN) : null, | |
| 71 | inboxTotal: items?.length ?? 0, | |
| 72 | reviews, | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 73 | }; |
| 74 | } | |
| 75 | ||
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 76 | /** |
| 77 | * Ask g1t: the direct message with the workspace's orchestrator, opened or | |
| 78 | * made, with what was asked posted in it; then that conversation. | |
| 79 | */ | |
| 80 | export async function action({ params, context, request }: Route.ActionArgs) { | |
| 81 | assertSameOrigin(request); | |
| 82 | const viewer = requireUser(context, request); | |
| 83 | const slug = params.owner.toLowerCase(); | |
| 84 | if (!roleIn(viewer, slug)) throw data(null, { status: 404 }); | |
| 85 | const form = await request.formData(); | |
| 86 | const body = String(form.get("body") ?? "").trim(); | |
| 87 | if (!body) return { error: "Write what you need." }; | |
| 88 | const listed = await workspaceAgents.list(slug, viewer).catch(() => null); | |
| 89 | const g1t = listed?.ok ? listed.value.find(isOrchestrator) : null; | |
| 90 | if (!g1t) return { error: "g1t isn't reachable right now. Try again in a moment." }; | |
| 91 | const dm = await chat.openDm(slug, viewer, [{ kind: "agent", id: g1t.id }]).catch(() => null); | |
| 92 | if (!dm?.ok) return { error: dm ? dm.error.message : "Chat didn't answer. Try again in a moment." }; | |
| 93 | const posted = await chat.post(slug, dm.value.id, viewer, { body }).catch(() => null); | |
| 94 | if (!posted?.ok) return { error: posted ? posted.error.message : "Your message didn't send. Try again." }; | |
| 95 | throw redirect(channelPath(slug, dm.value)); | |
| 96 | } | |
| 97 | ||
| 98 | function Section({ | |
| 99 | icon, | |
| 100 | title, | |
| 101 | count, | |
| 102 | to, | |
| 103 | action, | |
| 104 | children, | |
| 105 | }: { | |
| 106 | icon: ReactNode; | |
| 107 | title: string; | |
| 108 | count?: number | null; | |
| 109 | to: string; | |
| 110 | action: string; | |
| 111 | children: ReactNode; | |
| 112 | }) { | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 113 | return ( |
| 114 | <section className="flex flex-col rounded-2xl border border-line bg-surface"> | |
| 115 | <header className="flex items-center gap-2.5 border-b border-line px-5 py-3.5"> | |
| 116 | <span className="text-muted">{icon}</span> | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 117 | <h3 className="text-sm font-semibold">{title}</h3> |
| 118 | {count != null && count > 0 && ( | |
| 119 | <span className="rounded-full bg-accent/15 px-1.5 text-[0.6875rem] font-semibold text-accent tabular-nums">{count}</span> | |
| 120 | )} | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 121 | <Link to={to} className="ml-auto flex items-center gap-1 text-xs text-muted hover:text-fg"> |
| 122 | {action} | |
| 123 | <ArrowRight size={12} /> | |
| 124 | </Link> | |
| 125 | </header> | |
| 126 | <div className="grow p-2">{children}</div> | |
| 127 | </section> | |
| 128 | ); | |
| 129 | } | |
| 130 | ||
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 131 | function Quiet({ children }: { children: ReactNode }) { |
| 132 | return <p className="px-3 py-6 text-sm text-muted">{children}</p>; | |
| 133 | } | |
| 134 | ||
| 135 | function ChatRow({ entry, slug }: { entry: ChatSidebarEntry; slug: string }) { | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 136 | const other = entry.others[0]; |
| 137 | return ( | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 138 | <Link to={channelPath(slug, entry.channel)} className="flex h-11 items-center gap-3 rounded-lg px-3 transition-colors hover:bg-raised/60"> |
| 139 | <span className="flex w-6 justify-center"> | |
| 140 | {entry.channel.kind === "dm" && other ? <MemberAvatar member={other} size={22} /> : <span className="font-semibold text-faint">#</span>} | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 141 | </span> |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 142 | <span className="min-w-0 grow"> |
| 143 | <span className="block truncate text-sm font-medium text-fg">{entry.title}</span> | |
| 144 | <span className="block truncate text-xs text-faint"> | |
| 145 | {entry.mentions > 0 ? `${entry.mentions} ${entry.mentions === 1 ? "mention" : "mentions"}` : `${entry.unread} unread`} | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 146 | </span> |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 147 | </span> |
| 148 | <span | |
| 149 | className={`rounded-full px-1.5 text-[0.6875rem] leading-[1.125rem] font-semibold tabular-nums ${entry.mentions > 0 ? "bg-accent text-bg" : "bg-line-strong text-fg"}`} | |
| 150 | > | |
| 151 | {entry.mentions || entry.unread} | |
| 152 | </span> | |
| 153 | </Link> | |
| 154 | ); | |
| 155 | } | |
| 156 | ||
| 157 | function AgentRow({ agent, slug }: { agent: WorkspaceAgent; slug: string }) { | |
| 158 | return ( | |
| 159 | <Link to={`/${slug}/-/agents/${agent.handle}`} className="flex h-12 items-center gap-3 rounded-lg px-3 transition-colors hover:bg-raised/60"> | |
| 160 | <AgentFace agent={{ ...agent, builtin: isOrchestrator(agent) }} size={26} /> | |
| 161 | <span className="min-w-0 grow"> | |
| 162 | <span className="block truncate text-sm font-medium">{agent.display_name}</span> | |
| 163 | <span className="block truncate text-xs text-faint">{agent.role}</span> | |
| 164 | </span> | |
| 165 | <span className="flex shrink-0 items-center gap-1.5 text-xs text-muted"> | |
| 166 | <StatusDot status={agent.status} /> | |
| 167 | {statusLabel(agent.status)} | |
| 168 | </span> | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 169 | </Link> |
| 170 | ); | |
| 171 | } | |
| 172 | ||
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 173 | function ReviewRow({ row }: { row: NeedRow }) { |
| Chat and workspace agents: channels, DMs and named agents you talk to | 174 | return ( |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 175 | <Link to={row.to} className="flex items-start gap-3 rounded-lg px-3 py-2.5 transition-colors hover:bg-raised/60"> |
| 176 | <GitPullRequest size={16} className="mt-0.5 shrink-0 text-warn" /> | |
| 177 | <span className="min-w-0 grow"> | |
| 178 | <span className="block truncate text-sm font-medium text-fg">{row.title}</span> | |
| Merge branch 'site-fixes' into fast-push | 179 | <span className="line-clamp-2 text-xs text-muted"> |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 180 | {row.repo ? `${row.repo.name}${row.ref ? ` ${row.ref}` : ""} · ` : ""} |
| 181 | {row.ask} | |
| 182 | </span> | |
| 183 | </span> | |
| 184 | </Link> | |
| 185 | ); | |
| 186 | } | |
| 187 | ||
| 188 | /** Ask g1t: the orchestrator, from Home. Enter sends; Shift+Enter is a new line. */ | |
| 189 | function AskG1t({ slug, available, error }: { slug: string; available: boolean; error: string | null }) { | |
| 190 | const navigation = useNavigation(); | |
| 191 | const busy = navigation.state !== "idle" && navigation.formAction === `/${slug}/-/home`; | |
| 192 | const [text, setText] = useState(""); | |
| 193 | return ( | |
| 194 | <Form method="post" action={`/${slug}/-/home`} className="rounded-2xl border border-line-strong bg-surface transition-colors focus-within:border-accent/40"> | |
| 195 | <div className="flex items-center gap-2 px-4 pt-3.5 text-sm font-medium"> | |
| 196 | <G1tMark size={20} /> | |
| 197 | Ask g1t | |
| 198 | <span className="hidden font-normal text-faint sm:inline">It knows the team and hands work to the right agent.</span> | |
| 199 | </div> | |
| 200 | <textarea | |
| 201 | name="body" | |
| 202 | value={text} | |
| 203 | onChange={(event) => setText(event.target.value)} | |
| 204 | onKeyDown={(event) => { | |
| 205 | if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) { | |
| 206 | event.preventDefault(); | |
| 207 | if (text.trim()) event.currentTarget.form?.requestSubmit(); | |
| 208 | } | |
| 209 | }} | |
| 210 | rows={2} | |
| 211 | disabled={!available} | |
| 212 | aria-label="Ask g1t" | |
| 213 | placeholder={available ? "Get the flaky checks fixed and tell #support when it ships…" : "g1t isn't reachable right now."} | |
| 214 | className="block w-full resize-none bg-transparent px-4 pt-2.5 pb-1 text-[0.9375rem] leading-relaxed text-fg outline-none placeholder:text-faint disabled:cursor-not-allowed" | |
| 215 | /> | |
| 216 | <div className="flex items-center gap-3 px-3 pb-3"> | |
| 217 | {error ? <p className="text-sm text-danger">{error}</p> : <p className="hidden pl-1 text-xs text-faint sm:block">Opens your conversation with g1t in Chat.</p>} | |
| 218 | <button | |
| 219 | type="submit" | |
| 220 | disabled={!available || busy || !text.trim()} | |
| 221 | className="ml-auto inline-flex h-8 items-center gap-1.5 rounded-lg bg-accent px-3 text-sm font-medium text-bg transition-colors hover:bg-accent-hover disabled:bg-line disabled:text-faint" | |
| 222 | > | |
| 223 | {busy ? "Sending…" : "Ask"} | |
| 224 | <ArrowRight size={14} /> | |
| 225 | </button> | |
| 226 | </div> | |
| 227 | </Form> | |
| 228 | ); | |
| 229 | } | |
| 230 | ||
| 231 | export default function WorkspaceHome({ loaderData, actionData }: Route.ComponentProps) { | |
| 232 | const { slug, code, name, chat: mentions, agents, orchestrator, inbox: items, inboxTotal, reviews } = loaderData; | |
| 233 | const [greeting, setGreeting] = useState(loaderData.greeting); | |
| 234 | // The reader's own clock. | |
| 235 | useEffect(() => setGreeting(greetingFor(new Date().getHours())), []); | |
| 236 | const waiting = agents?.filter((agent) => agent.status === "waiting") ?? null; | |
| 237 | return ( | |
| 238 | <div className="mx-auto max-w-5xl space-y-8"> | |
| 239 | <header> | |
| 240 | <h1 className="text-[1.75rem] leading-tight font-semibold tracking-tight" suppressHydrationWarning> | |
| 241 | {greeting}, {name} | |
| 242 | </h1> | |
| 243 | <p className="mt-1.5 text-sm text-muted">Everything that needs you, across chat, agents{code ? ", code" : ""} and your inbox.</p> | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 244 | </header> |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 245 | |
| 246 | <AskG1t slug={slug} available={orchestrator != null} error={actionData?.error ?? null} /> | |
| 247 | ||
| 248 | <div> | |
| 249 | <h2 className="mb-3 text-sm font-semibold text-fg-soft">Needs you across everything</h2> | |
| 250 | <div className="grid gap-4 lg:grid-cols-2"> | |
| 251 | <Section icon={<AtSign size={15} />} title="Mentions and DMs" count={mentions?.length} to={`/${slug}/-/chat`} action="Open Chat"> | |
| 252 | {mentions == null ? ( | |
| 253 | <Quiet>Chat didn't answer. Your mentions show here once it does.</Quiet> | |
| 254 | ) : mentions.length === 0 ? ( | |
| 255 | <Quiet>No unread mentions or messages.</Quiet> | |
| 256 | ) : ( | |
| 257 | mentions.slice(0, ROWS).map((entry) => <ChatRow key={entry.channel.id} entry={entry} slug={slug} />) | |
| 258 | )} | |
| 259 | </Section> | |
| 260 | <Section icon={<Sparkles size={15} />} title="Agents waiting on you" count={waiting?.length} to={`/${slug}/-/agents`} action="Open Agents"> | |
| 261 | {waiting == null ? ( | |
| 262 | <Quiet>The agents service didn't answer.</Quiet> | |
| 263 | ) : waiting.length === 0 ? ( | |
| 264 | <Quiet>No agent is waiting on you.</Quiet> | |
| 265 | ) : ( | |
| 266 | waiting.slice(0, ROWS).map((agent) => <AgentRow key={agent.id} agent={agent} slug={slug} />) | |
| 267 | )} | |
| 268 | </Section> | |
| 269 | {code && reviews && ( | |
| 270 | <Section icon={<GitPullRequest size={15} />} title="Reviews and approvals" to={`/${slug}/-/overview`} action="Open Code"> | |
| 271 | <Suspense fallback={<RowsSkeleton />}> | |
| 272 | <Await resolve={reviews}> | |
| 273 | {(rows) => | |
| 274 | rows == null ? ( | |
| 275 | <Quiet>Code's reviews couldn't be read right now.</Quiet> | |
| 276 | ) : rows.length === 0 ? ( | |
| 277 | <Quiet>Nothing to review or approve.</Quiet> | |
| 278 | ) : ( | |
| 279 | rows.map((row) => <ReviewRow key={row.key} row={row} />) | |
| 280 | ) | |
| 281 | } | |
| 282 | </Await> | |
| 283 | </Suspense> | |
| 284 | </Section> | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 285 | )} |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 286 | <Section icon={<Inbox size={15} />} title="From your inbox" count={inboxTotal} to="/inbox" action="Open Inbox"> |
| 287 | {items == null ? ( | |
| 288 | <Quiet>Your inbox couldn't be read right now.</Quiet> | |
| 289 | ) : items.length === 0 ? ( | |
| 290 | <Quiet>Nothing unread.</Quiet> | |
| 291 | ) : ( | |
| 292 | <ul className="space-y-2 p-1"> | |
| 293 | {items.map((item) => ( | |
| 294 | <InboxCard key={item.id} item={item} /> | |
| 295 | ))} | |
| 296 | </ul> | |
| 297 | )} | |
| 298 | </Section> | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 299 | </div> |
| 300 | </div> | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 301 | {!code && ( |
| 302 | <p className="flex items-center gap-2 text-xs text-faint"> | |
| 303 | <MessagesSquare size={13} /> | |
| 304 | Your membership includes Chat, Docs, Agents and the Inbox. | |
| 305 | </p> | |
| 306 | )} | |
| 307 | </div> | |
| 308 | ); | |
| 309 | } | |
| 310 | ||
| 311 | function RowsSkeleton() { | |
| 312 | return ( | |
| 313 | <div className="space-y-3 px-3 py-3" aria-busy="true"> | |
| 314 | {[70, 55, 62].map((w) => ( | |
| 315 | <div key={w} className="flex items-center gap-3"> | |
| 316 | <Skeleton className="size-4 rounded" /> | |
| 317 | <Skeleton className="h-3" style={{ width: `${w}%` }} /> | |
| 318 | </div> | |
| 319 | ))} | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 320 | </div> |
| 321 | ); | |
| 322 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.