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 and workspace agents: channels, DMs and named agents you talk to | 1 | import { Check, Hash, Lock, Plus, Search, SquarePen, X } from "lucide-react"; |
| 2 | import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"; | |
| 3 | import { useFetcher, useNavigate, useRouteLoaderData } from "react-router"; | |
| 4 | ||
| 5 | import type { ChatSidebar, Result } from "@g1t/contracts"; | |
| 6 | ||
| 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) | 7 | import { AgentPill } from "./marks"; |
| 8 | import { AgentAvatar } from "../agent-avatar"; | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 9 | |
| 10 | import { Avatar } from "../ui/avatar"; | |
| 11 | import { Button } from "../ui/button"; | |
| 12 | import { Card } from "../ui/card"; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 13 | import { Hint } from "../ui/hint"; |
| 14 | import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog"; | |
| 15 | import { Switch } from "../ui/switch"; | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 16 | import { channelName, shownHandle, shownName } from "../../lib/chat"; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 17 | import type { ChatLayoutData } from "../../routes/workspace/chat/layout"; |
| 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) | 18 | import { useLiveCounts } from "../../lib/notify-client"; |
| 19 | import { overlayEntries, unlisted } from "../../lib/notify-store"; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 20 | |
| 21 | /** Chat mode's data, while a chat page is open; undefined elsewhere. */ | |
| 22 | export function useChatData(): ChatLayoutData | undefined { | |
| 23 | return useRouteLoaderData("routes/workspace/chat/layout") as ChatLayoutData | undefined; | |
| 24 | } | |
| 25 | ||
| 26 | /** The sidebar's own fetcher: every component that refreshes it shares this one. */ | |
| 27 | export const SIDEBAR_FETCHER = "chat-sidebar"; | |
| 28 | ||
| 29 | /** The sidebar as last read: refreshed in place, else what the page loaded. */ | |
| 30 | export function useChatSidebar(): { sidebar: ChatSidebar | null; refresh: () => void } { | |
| 31 | const data = useChatData(); | |
| 32 | const fetcher = useFetcher<Result<ChatSidebar>>({ key: SIDEBAR_FETCHER }); | |
| 33 | const slug = data?.slug; | |
| 34 | const refresh = useCallback(() => { | |
| 35 | if (slug) fetcher.load(`/${slug}/-/chat/api`); | |
| 36 | // eslint-disable-next-line react-hooks/exhaustive-deps | |
| 37 | }, [slug]); | |
| 38 | const fresh = fetcher.data?.ok ? fetcher.data.value : null; | |
| 39 | const sidebar = fresh ?? data?.sidebar ?? null; | |
| 40 | // A conversation read on its page reads as read everywhere at once, until | |
| 41 | // the service's next answer says so itself. | |
| 42 | const [read, setRead] = useState<ReadonlySet<string>>(new Set()); | |
| 43 | useEffect(() => { | |
| 44 | const onRead = (event: Event) => setRead((now) => new Set(now).add(String((event as CustomEvent).detail))); | |
| 45 | window.addEventListener("g1t:chat-read", onRead); | |
| 46 | return () => window.removeEventListener("g1t:chat-read", onRead); | |
| 47 | }, []); | |
| 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) | 48 | // Notify: the feed socket's counts (lib/notify-client.ts) over the sidebar as read, live. |
| 49 | const live = useLiveCounts(slug); | |
| 50 | useEffect(() => setRead(new Set()), [sidebar, live]); | |
| 51 | const shown = useMemo(() => { | |
| 52 | const counted = sidebar && live ? { ...sidebar, entries: overlayEntries(sidebar.entries, live) } : sidebar; | |
| 53 | return counted && read.size > 0 | |
| 54 | ? { ...counted, entries: counted.entries.map((e) => (read.has(e.channel.id) ? { ...e, unread: 0, mentions: 0 } : e)) } | |
| 55 | : counted; | |
| 56 | }, [sidebar, read, live]); | |
| 57 | // A conversation the feed counts that the sidebar does not list (a new DM): read the sidebar again. | |
| 58 | const missing = sidebar ? unlisted(sidebar.entries, live).join(",") : ""; | |
| 59 | useEffect(() => { | |
| 60 | if (missing) refresh(); | |
| 61 | }, [missing, refresh]); | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 62 | return { sidebar: shown, refresh }; |
| 63 | } | |
| 64 | ||
| 65 | /** | |
| 66 | * Sends one request to the chat's JSON route and answers with the | |
| 67 | * service's result; a network failure is a failure like any other. Doing | |
| 68 | * something that changes the sidebar refreshes it. | |
| 69 | */ | |
| 70 | export function useChatSend(slug: string) { | |
| 71 | const fetcher = useFetcher<Result<ChatSidebar>>({ key: SIDEBAR_FETCHER }); | |
| 72 | return useCallback( | |
| 73 | async <T = unknown,>(body: Record<string, unknown>): Promise<Result<T>> => { | |
| 74 | try { | |
| 75 | const response = await fetch(`/${slug}/-/chat/api`, { | |
| 76 | method: "POST", | |
| 77 | headers: { "content-type": "application/json" }, | |
| 78 | body: JSON.stringify(body), | |
| 79 | }); | |
| 80 | const result = (await response.json()) as Result<T>; | |
| 81 | if (result.ok && body.intent !== "post" && body.intent !== "read") fetcher.load(`/${slug}/-/chat/api`); | |
| 82 | return result; | |
| 83 | } catch { | |
| 84 | return { ok: false, error: { code: "conflict", message: "Couldn't reach g1t. Check your connection and try again." } }; | |
| 85 | } | |
| 86 | }, | |
| 87 | // eslint-disable-next-line react-hooks/exhaustive-deps | |
| 88 | [slug], | |
| 89 | ); | |
| 90 | } | |
| 91 | ||
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 92 | /** Why someone may not make a channel: the workspace keeps it to its owners. */ |
| 93 | export const OWNERS_ONLY_CHANNELS = "Only workspace owners can create channels here."; | |
| 94 | ||
| 95 | /** | |
| 96 | * What the viewer may make, from the workspace's chat settings (Settings, | |
| 97 | * Chat). Until the sidebar arrives, everything: the chat service checks. | |
| 98 | */ | |
| 99 | export function useChannelCreation(): { public: boolean; private: boolean; any: boolean } { | |
| 100 | const { sidebar } = useChatSidebar(); | |
| 101 | const open = sidebar?.can?.create_public_channels ?? true; | |
| 102 | const closed = sidebar?.can?.create_private_channels ?? true; | |
| 103 | return { public: open, private: closed, any: open || closed }; | |
| 104 | } | |
| 105 | ||
| 106 | /** | |
| 107 | * Making a channel: its name, what it is for, and whether it is private. | |
| 108 | * Someone the workspace's settings do not let make one sees the button | |
| 109 | * turned off, with why; someone who may make only one kind gets that kind. | |
| 110 | */ | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 111 | export function CreateChannelButton({ slug, variant = "icon" }: { slug: string; variant?: "icon" | "button" }) { |
| 112 | const [open, setOpen] = useState(false); | |
| 113 | const [name, setName] = useState(""); | |
| 114 | const [topic, setTopic] = useState(""); | |
| 115 | const [secret, setSecret] = useState(false); | |
| 116 | const [error, setError] = useState<string | null>(null); | |
| 117 | const [busy, setBusy] = useState(false); | |
| 118 | const send = useChatSend(slug); | |
| 119 | const navigate = useNavigate(); | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 120 | const may = useChannelCreation(); |
| 121 | // Only one kind allowed: that kind, and the switch says why it is fixed. | |
| 122 | const isPrivate = !may.public ? true : !may.private ? false : secret; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 123 | const clean = channelName(name); |
| 124 | const submit = async () => { | |
| 125 | if (!clean) return setError("Give the channel a name."); | |
| 126 | setBusy(true); | |
| 127 | setError(null); | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 128 | const made = await send<{ to: string }>({ intent: "create_channel", name: clean, topic, private: isPrivate }); |
| Chat and workspace agents: channels, DMs and named agents you talk to | 129 | setBusy(false); |
| 130 | if (!made.ok) return setError(made.error.message); | |
| 131 | setOpen(false); | |
| 132 | setName(""); | |
| 133 | setTopic(""); | |
| 134 | navigate(made.value.to); | |
| 135 | }; | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 136 | if (!may.any) { |
| 137 | // Shown off, not hidden, so nobody wonders where it went. | |
| 138 | return ( | |
| 139 | <Hint label={OWNERS_ONLY_CHANNELS} disabled> | |
| 140 | {variant === "icon" ? ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 141 | <Button type="button" aria-label="Create a channel" disabled variant="ghost" size="icon-xs" className="text-faint opacity-40"> |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 142 | <Plus size={14} /> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 143 | </Button> |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 144 | ) : ( |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 145 | <Button type="button" disabled variant="outline" className="opacity-50"> |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 146 | <Lock size={14} /> |
| 147 | Create a channel | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 148 | </Button> |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 149 | )} |
| 150 | </Hint> | |
| 151 | ); | |
| 152 | } | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 153 | return ( |
| 154 | <> | |
| 155 | {variant === "icon" ? ( | |
| 156 | <Hint label="Create a channel"> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 157 | <Button type="button" aria-label="Create a channel" onClick={() => setOpen(true)} variant="ghost" size="icon-xs" className="text-faint"> |
| Chat and workspace agents: channels, DMs and named agents you talk to | 158 | <Plus size={14} /> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 159 | </Button> |
| Chat and workspace agents: channels, DMs and named agents you talk to | 160 | </Hint> |
| 161 | ) : ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 162 | <Button type="button" onClick={() => setOpen(true)} variant="outline"> |
| Chat and workspace agents: channels, DMs and named agents you talk to | 163 | <Hash size={15} /> |
| 164 | Create a channel | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 165 | </Button> |
| Chat and workspace agents: channels, DMs and named agents you talk to | 166 | )} |
| 167 | <Dialog open={open} onOpenChange={setOpen}> | |
| 168 | <DialogContent> | |
| 169 | <DialogHeader> | |
| 170 | <DialogTitle>Create a channel</DialogTitle> | |
| 171 | <DialogDescription>A place for one topic: a project, a team, a launch. Invite people and agents once it's made.</DialogDescription> | |
| 172 | </DialogHeader> | |
| 173 | <form | |
| 174 | className="grid gap-4" | |
| 175 | onSubmit={(event) => { | |
| 176 | event.preventDefault(); | |
| 177 | void submit(); | |
| 178 | }} | |
| 179 | > | |
| 180 | <label className="grid gap-1.5"> | |
| 181 | <span className="text-sm font-medium text-muted">Name</span> | |
| 182 | <span className="flex h-10 items-center gap-1.5 rounded-md border border-line bg-bg px-3 focus-within:border-accent-dim"> | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 183 | {isPrivate ? <Lock size={14} className="text-faint" /> : <Hash size={14} className="text-faint" />} |
| Chat and workspace agents: channels, DMs and named agents you talk to | 184 | <input |
| 185 | autoFocus | |
| 186 | value={name} | |
| 187 | onChange={(event) => setName(event.target.value)} | |
| 188 | placeholder="release-train" | |
| 189 | maxLength={80} | |
| 190 | autoComplete="off" | |
| 191 | data-1p-ignore | |
| 192 | className="min-w-0 grow bg-transparent text-sm outline-none placeholder:text-faint" | |
| 193 | /> | |
| 194 | </span> | |
| 195 | {name && clean !== name && <span className="text-xs text-faint">It will be #{clean || "…"}</span>} | |
| 196 | </label> | |
| 197 | <label className="grid gap-1.5"> | |
| 198 | <span className="text-sm font-medium text-muted"> | |
| 199 | Topic <span className="font-normal text-faint">(optional)</span> | |
| 200 | </span> | |
| 201 | <input | |
| 202 | value={topic} | |
| 203 | onChange={(event) => setTopic(event.target.value)} | |
| 204 | placeholder="What this channel is for" | |
| 205 | maxLength={250} | |
| 206 | autoComplete="off" | |
| 207 | data-1p-ignore | |
| 208 | className="h-10 rounded-md border border-line bg-bg px-3 text-sm outline-none placeholder:text-faint focus:border-accent-dim" | |
| 209 | /> | |
| 210 | </label> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 211 | <Card asChild tone="plain" radius="lg" className="flex items-start justify-between gap-4 bg-bg/60 p-3"> |
| 212 | <label> | |
| 213 | <span> | |
| 214 | <span className="flex items-center gap-1.5 text-sm font-medium"> | |
| 215 | {isPrivate ? <Lock size={13} className="text-muted" /> : <Hash size={13} className="text-muted" />} | |
| 216 | {isPrivate ? "Private" : "Public"} | |
| 217 | </span> | |
| 218 | <span className="mt-0.5 block text-xs text-muted"> | |
| 219 | {!may.public | |
| 220 | ? "Only workspace owners can create public channels here, so this one is private: only people and agents you invite can find and read it." | |
| 221 | : !may.private | |
| 222 | ? "Only workspace owners can create private channels here, so this one is public: anyone in the workspace can find, read and join it." | |
| 223 | : isPrivate | |
| 224 | ? "Only people and agents you invite can find and read it." | |
| 225 | : "Anyone in the workspace can find, read and join it. Turn on to keep it to the people and agents you invite."} | |
| 226 | </span> | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 227 | </span> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 228 | <Switch checked={isPrivate} onCheckedChange={setSecret} disabled={!may.public || !may.private} aria-label="Private" /> |
| 229 | </label> | |
| 230 | </Card> | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 231 | {error && <p className="text-sm text-danger">{error}</p>} |
| 232 | <DialogFooter> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 233 | <Button type="button" onClick={() => setOpen(false)} variant="outline"> |
| Chat and workspace agents: channels, DMs and named agents you talk to | 234 | Cancel |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 235 | </Button> |
| 236 | <Button | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 237 | type="submit" |
| 238 | disabled={busy || !clean} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 239 | variant="accent" |
| Chat and workspace agents: channels, DMs and named agents you talk to | 240 | > |
| 241 | {busy ? "Creating…" : "Create channel"} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 242 | </Button> |
| Chat and workspace agents: channels, DMs and named agents you talk to | 243 | </DialogFooter> |
| 244 | </form> | |
| 245 | </DialogContent> | |
| 246 | </Dialog> | |
| 247 | </> | |
| 248 | ); | |
| 249 | } | |
| 250 | ||
| 251 | /** Someone a new message can go to. */ | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 252 | type Pickable = { key: string; kind: "user" | "agent"; name: string; handle: string; display: string; avatar: string | null; role?: string | null }; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 253 | |
| 254 | /** | |
| 255 | * A new direct message: choose people, agents or both, then open the | |
| 256 | * conversation between you (the same one each time for the same people). | |
| 257 | */ | |
| 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) | 258 | export function NewMessageButton({ slug, variant = "icon", children }: { slug: string; variant?: "icon" | "button" | "fab"; children?: ReactNode }) { |
| Chat and workspace agents: channels, DMs and named agents you talk to | 259 | const [open, setOpen] = useState(false); |
| 260 | return ( | |
| 261 | <> | |
| 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) | 262 | {variant === "fab" ? ( |
| 263 | // A phone's list: the round button above the tab bar. | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 264 | <Button |
| 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) | 265 | type="button" |
| 266 | aria-label="New message" | |
| 267 | onClick={() => setOpen(true)} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 268 | variant="accent" size="icon" |
| 269 | className="size-14 rounded-2xl shadow-lg shadow-black/50 transition-transform active:scale-95" | |
| 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) | 270 | > |
| 271 | <SquarePen size={22} /> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 272 | </Button> |
| 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) | 273 | ) : variant === "icon" ? ( |
| Chat and workspace agents: channels, DMs and named agents you talk to | 274 | <Hint label="New message"> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 275 | <Button type="button" aria-label="New message" onClick={() => setOpen(true)} variant="ghost" size="icon-sm" className="text-faint"> |
| Chat and workspace agents: channels, DMs and named agents you talk to | 276 | <SquarePen size={15} /> |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 277 | </Button> |
| Chat and workspace agents: channels, DMs and named agents you talk to | 278 | </Hint> |
| 279 | ) : ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 280 | <Button type="button" onClick={() => setOpen(true)} variant="outline"> |
| Chat and workspace agents: channels, DMs and named agents you talk to | 281 | <SquarePen size={15} /> |
| 282 | {children ?? "New message"} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 283 | </Button> |
| Chat and workspace agents: channels, DMs and named agents you talk to | 284 | )} |
| 285 | <Dialog open={open} onOpenChange={setOpen}> | |
| Chat on a phone: the list and its new-message button follow the real top and tab bar heights; the new-message sheet keeps clear of the home indicator without extra padding | 286 | <DialogContent className="max-w-md gap-0 p-0 max-sm:pb-[env(safe-area-inset-bottom)]" showClose={false}> |
| Chat and workspace agents: channels, DMs and named agents you talk to | 287 | {open && <NewMessage slug={slug} onDone={() => setOpen(false)} />} |
| 288 | </DialogContent> | |
| 289 | </Dialog> | |
| 290 | </> | |
| 291 | ); | |
| 292 | } | |
| 293 | ||
| 294 | function NewMessage({ slug, onDone }: { slug: string; onDone: () => void }) { | |
| 295 | const data = useChatData(); | |
| 296 | const [query, setQuery] = useState(""); | |
| 297 | const [chosen, setChosen] = useState<Pickable[]>([]); | |
| 298 | const [active, setActive] = useState(0); | |
| 299 | const [error, setError] = useState<string | null>(null); | |
| 300 | const [busy, setBusy] = useState(false); | |
| 301 | const input = useRef<HTMLInputElement>(null); | |
| 302 | const send = useChatSend(slug); | |
| 303 | const navigate = useNavigate(); | |
| 304 | const everyone = useMemo<Pickable[]>(() => { | |
| 305 | const me = data?.me.username.toLowerCase(); | |
| 306 | return [ | |
| Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how. | 307 | // A personal agent is only in its own DM, opened from the sidebar, never with others. |
| 308 | ...(data?.agents ?? []).filter((agent) => agent.scope !== "personal").map((agent) => ({ | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 309 | key: `agent:${agent.id}`, |
| 310 | kind: "agent" as const, | |
| 311 | name: agent.handle, | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 312 | handle: agent.handle, |
| Chat and workspace agents: channels, DMs and named agents you talk to | 313 | display: agent.display_name, |
| 314 | avatar: agent.avatar, | |
| 315 | role: agent.role, | |
| 316 | })), | |
| 317 | ...(data?.people ?? []) | |
| 318 | .filter((person) => person.name.toLowerCase() !== me) | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 319 | .map((person) => ({ key: `user:${person.name}`, kind: "user" as const, name: person.name, handle: shownHandle(person), display: shownName(person), avatar: person.avatar })), |
| Chat and workspace agents: channels, DMs and named agents you talk to | 320 | ]; |
| 321 | }, [data]); | |
| 322 | const q = query.trim().toLowerCase().replace(/^@/, ""); | |
| 323 | const picked = new Set(chosen.map((p) => p.key)); | |
| 324 | const options = everyone.filter( | |
| 325 | (p) => !picked.has(p.key) && (!q || p.name.toLowerCase().includes(q) || p.display.toLowerCase().includes(q)), | |
| 326 | ); | |
| 327 | const take = (person: Pickable) => { | |
| 328 | setChosen((now) => [...now, person]); | |
| 329 | setQuery(""); | |
| 330 | setActive(0); | |
| 331 | input.current?.focus(); | |
| 332 | }; | |
| 333 | const go = async () => { | |
| 334 | if (chosen.length === 0) return; | |
| 335 | setBusy(true); | |
| 336 | setError(null); | |
| 337 | const opened = await send<{ to: string }>({ intent: "dm", members: chosen.map((p) => p.key) }); | |
| 338 | setBusy(false); | |
| 339 | if (!opened.ok) return setError(opened.error.message); | |
| 340 | onDone(); | |
| 341 | navigate(opened.value.to); | |
| 342 | }; | |
| 343 | return ( | |
| 344 | <div className="flex max-h-[min(36rem,calc(100dvh-2rem))] flex-col"> | |
| 345 | <div className="border-b border-line px-5 pt-5 pb-4"> | |
| 346 | <DialogTitle>New message</DialogTitle> | |
| 347 | <DialogDescription className="mt-1">To one person, a few, or an agent. Agents answer in the conversation.</DialogDescription> | |
| 348 | <div className="mt-4 flex min-h-10 flex-wrap items-center gap-1.5 rounded-md border border-line bg-bg px-2 py-1.5 focus-within:border-accent-dim"> | |
| 349 | <span className="pl-1 text-sm text-faint">To:</span> | |
| 350 | {chosen.map((person) => ( | |
| 351 | <span key={person.key} className="flex h-6 items-center gap-1.5 rounded-md bg-raised pr-1 pl-1 text-[0.8125rem]"> | |
| 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) | 352 | {person.kind === "agent" ? <AgentAvatar agent={{ id: person.key.slice(6), handle: person.name, avatar: person.avatar }} size={16} /> : <Avatar name={person.name} image={person.avatar} size={16} />} |
| Chat and workspace agents: channels, DMs and named agents you talk to | 353 | {person.display} |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 354 | <Button |
| Chat and workspace agents: channels, DMs and named agents you talk to | 355 | type="button" |
| 356 | aria-label={`Remove ${person.display}`} | |
| 357 | onClick={() => setChosen((now) => now.filter((p) => p.key !== person.key))} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 358 | variant="ghost" |
| 359 | size="inline" | |
| 360 | className="rounded p-0.5 text-faint hover:bg-line" | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 361 | > |
| 362 | <X size={12} /> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 363 | </Button> |
| Chat and workspace agents: channels, DMs and named agents you talk to | 364 | </span> |
| 365 | ))} | |
| 366 | <input | |
| 367 | ref={input} | |
| 368 | autoFocus | |
| 369 | value={query} | |
| 370 | onChange={(event) => { | |
| 371 | setQuery(event.target.value); | |
| 372 | setActive(0); | |
| 373 | }} | |
| 374 | onKeyDown={(event) => { | |
| 375 | if (event.key === "ArrowDown" || event.key === "ArrowUp") { | |
| 376 | event.preventDefault(); | |
| 377 | const count = Math.max(1, options.length); | |
| 378 | setActive((index) => (index + (event.key === "ArrowDown" ? 1 : count - 1)) % count); | |
| 379 | } else if (event.key === "Enter") { | |
| 380 | event.preventDefault(); | |
| 381 | const person = options[active]; | |
| 382 | if (person && q) take(person); | |
| 383 | else void go(); | |
| 384 | } else if (event.key === "Backspace" && !query && chosen.length) { | |
| 385 | setChosen((now) => now.slice(0, -1)); | |
| 386 | } | |
| 387 | }} | |
| 388 | placeholder={chosen.length ? "" : "Name or @handle"} | |
| 389 | aria-label="Who to message" | |
| 390 | autoComplete="off" | |
| 391 | data-1p-ignore | |
| 392 | className="h-7 min-w-24 grow bg-transparent px-1 text-sm outline-none placeholder:text-faint" | |
| 393 | /> | |
| 394 | </div> | |
| 395 | </div> | |
| 396 | <ul role="listbox" aria-label="People and agents" className="min-h-0 grow overflow-y-auto p-1.5"> | |
| 397 | {options.length === 0 && ( | |
| 398 | <li className="flex items-center gap-2 px-3 py-6 text-sm text-muted"> | |
| 399 | <Search size={14} /> | |
| 400 | {everyone.length === 0 ? "No one else is in this workspace yet." : "No one matches."} | |
| 401 | </li> | |
| 402 | )} | |
| 403 | {options.map((person, index) => ( | |
| 404 | <li | |
| 405 | key={person.key} | |
| 406 | role="option" | |
| 407 | aria-selected={index === active} | |
| 408 | onMouseEnter={() => setActive(index)} | |
| 409 | onClick={() => take(person)} | |
| 410 | className={`flex cursor-pointer items-center gap-3 rounded-md px-3 py-2 ${index === active ? "bg-raised" : ""}`} | |
| 411 | > | |
| 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) | 412 | {person.kind === "agent" ? <AgentAvatar agent={{ id: person.key.slice(6), handle: person.name, avatar: person.avatar }} size={28} /> : <Avatar name={person.name} image={person.avatar} size={28} />} |
| Chat and workspace agents: channels, DMs and named agents you talk to | 413 | <span className="min-w-0 grow"> |
| 414 | <span className="flex items-center gap-1.5 text-sm font-medium"> | |
| 415 | <span className="truncate">{person.display}</span> | |
| 416 | {person.kind === "agent" && <AgentPill />} | |
| 417 | </span> | |
| 418 | <span className="block truncate text-xs text-faint"> | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 419 | @{person.handle} |
| Chat and workspace agents: channels, DMs and named agents you talk to | 420 | {person.role ? ` · ${person.role}` : ""} |
| 421 | </span> | |
| 422 | </span> | |
| 423 | {index === active && <Check size={14} className="text-faint" />} | |
| 424 | </li> | |
| 425 | ))} | |
| 426 | </ul> | |
| 427 | <div className="flex items-center justify-between gap-3 border-t border-line px-5 py-3"> | |
| 428 | <span className="min-w-0 truncate text-sm text-danger">{error}</span> | |
| 429 | <div className="flex shrink-0 gap-2"> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 430 | <Button type="button" onClick={onDone} variant="outline"> |
| Chat and workspace agents: channels, DMs and named agents you talk to | 431 | Cancel |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 432 | </Button> |
| 433 | <Button | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 434 | type="button" |
| 435 | disabled={busy || chosen.length === 0} | |
| 436 | onClick={() => void go()} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 437 | variant="accent" |
| Chat and workspace agents: channels, DMs and named agents you talk to | 438 | > |
| 439 | {busy ? "Opening…" : "Open conversation"} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 440 | </Button> |
| Chat and workspace agents: channels, DMs and named agents you talk to | 441 | </div> |
| 442 | </div> | |
| 443 | </div> | |
| 444 | ); | |
| 445 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.