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 { |
| 2 | Activity, | |
| 3 | BarChart3, | |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 4 | BellOff, |
| 5 | BellRing, | |
| 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) | 6 | BookOpen, |
| 7 | Building2, | |
| 8 | Check, | |
| 9 | CircleUserRound, | |
| 10 | Code2, | |
| 11 | House, | |
| 12 | Inbox, | |
| 13 | LifeBuoy, | |
| 14 | LogOut, | |
| 15 | MessagesSquare, | |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 16 | Moon, |
| 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) | 17 | Plug, |
| 18 | Plus, | |
| 19 | Settings, | |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 20 | Smile, |
| 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) | 21 | Sparkles, |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 22 | Sun, |
| 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) | 23 | Users, |
| 24 | UsersRound, | |
| 25 | } from "lucide-react"; | |
| 26 | import { Dialog as Primitive } from "radix-ui"; | |
| 27 | import { type ReactNode, useEffect, useRef, useState } from "react"; | |
| 28 | import { Link, useLocation, useNavigate, useSubmit } from "react-router"; | |
| 29 | ||
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 30 | import { type Membership, type User, hasCodeAccess, shownUsername } from "@g1t/contracts"; |
| 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) | 31 | |
| 32 | import { Avatar } from "./ui"; | |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 33 | import { StatusDialog } from "./presence"; |
| 34 | import { setPresence, useOwnPresence } from "../lib/notify-client"; | |
| 35 | import { dndOn, liveStatus, pauseUntil, untilLabel } from "../lib/presence"; | |
| 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) | 36 | import { STATUS_URL } from "../lib/status"; |
| 37 | import { modeOf } from "../lib/workspace-nav"; | |
| 38 | ||
| 39 | /** | |
| 40 | * The phone's layout (below 768px): a tab bar along the bottom, the | |
| 41 | * workspace's avatar opening a sheet for everything else, and each tab a | |
| 42 | * list that pushes its detail full screen. Desktop keeps the rail. | |
| 43 | */ | |
| 44 | ||
| 45 | /** Whether a path is one conversation, which takes the whole screen on a phone. */ | |
| 46 | export function isConversation(pathname: string): boolean { | |
| 47 | return /^\/[^/]+\/-\/chat\/(?!browse\/?$)[^/]+/.test(pathname); | |
| 48 | } | |
| 49 | ||
| 50 | /** | |
| 51 | * Follows the visual viewport, which shrinks when the keyboard opens: its | |
| 52 | * height and top go on the page as `--vv-height` and `--vv-top`, and | |
| 53 | * `data-keyboard="open"` on <html> while the keyboard is up, so a | |
| 54 | * composer can sit just above it and the tab bar can step aside. | |
| 55 | */ | |
| 56 | export function useVisualViewport() { | |
| 57 | useEffect(() => { | |
| 58 | const viewport = window.visualViewport; | |
| 59 | if (!viewport) return; | |
| 60 | const root = document.documentElement; | |
| 61 | const update = () => { | |
| 62 | root.style.setProperty("--vv-height", `${viewport.height}px`); | |
| 63 | root.style.setProperty("--vv-top", `${viewport.offsetTop}px`); | |
| 64 | const keyboard = window.innerHeight - viewport.height > 140; | |
| 65 | if (keyboard) root.dataset.keyboard = "open"; | |
| 66 | else delete root.dataset.keyboard; | |
| 67 | }; | |
| 68 | update(); | |
| 69 | viewport.addEventListener("resize", update); | |
| 70 | viewport.addEventListener("scroll", update); | |
| 71 | return () => { | |
| 72 | viewport.removeEventListener("resize", update); | |
| 73 | viewport.removeEventListener("scroll", update); | |
| 74 | }; | |
| 75 | }, []); | |
| 76 | } | |
| 77 | ||
| 78 | /** | |
| 79 | * Swipe from the left edge to go back, as on a phone's own apps: a touch | |
| 80 | * that starts within 28px of the edge and travels 80px right, more across | |
| 81 | * than down. Going back is the browser's own back, so history stays true. | |
| 82 | */ | |
| 83 | export function useSwipeBack(onBack: () => void) { | |
| 84 | const start = useRef<{ x: number; y: number } | null>(null); | |
| 85 | return { | |
| 86 | onTouchStart: (event: React.TouchEvent) => { | |
| 87 | const touch = event.touches[0]; | |
| 88 | start.current = touch && touch.clientX < 28 ? { x: touch.clientX, y: touch.clientY } : null; | |
| 89 | }, | |
| 90 | onTouchMove: (event: React.TouchEvent) => { | |
| 91 | const touch = event.touches[0]; | |
| 92 | if (!start.current || !touch) return; | |
| 93 | const dx = touch.clientX - start.current.x; | |
| 94 | const dy = Math.abs(touch.clientY - start.current.y); | |
| 95 | if (dx > 80 && dx > dy * 1.5) { | |
| 96 | start.current = null; | |
| 97 | onBack(); | |
| 98 | } | |
| 99 | }, | |
| 100 | onTouchEnd: () => { | |
| 101 | start.current = null; | |
| 102 | }, | |
| 103 | }; | |
| 104 | } | |
| 105 | ||
| 106 | /** Back, as the browser would; to `fallback` when this page was the first one opened. */ | |
| 107 | export function useBack(fallback: string) { | |
| 108 | const navigate = useNavigate(); | |
| 109 | return () => { | |
| 110 | const index = (window.history.state as { idx?: number } | null)?.idx ?? 0; | |
| 111 | if (index > 0) navigate(-1); | |
| 112 | else navigate(fallback, { replace: true }); | |
| 113 | }; | |
| 114 | } | |
| 115 | ||
| 116 | type Tab = { key: string; label: string; to: string; icon: ReactNode; badge?: { count: number; loud: boolean } }; | |
| 117 | ||
| 118 | /** | |
| Code sits second in the rail and on a phone's tab bar: for members who have it, it is the everyday mode | 119 | * The tab bar along the bottom of a phone: Home, Code (Docs, for a member |
| 120 | * without Code), Chat, Agents and the Inbox, each with what is unread. It | |
| 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) | 121 | * steps aside while the keyboard is up and inside a conversation. |
| 122 | */ | |
| 123 | export function MobileTabBar({ workspace, unread }: { workspace: Membership; unread: { inbox: number; chat: number; mentions: number } }) { | |
| 124 | const { pathname } = useLocation(); | |
| 125 | const slug = workspace.slug; | |
| 126 | const code = hasCodeAccess(workspace); | |
| 127 | const mode = modeOf(pathname, slug); | |
| 128 | const tabs: Tab[] = [ | |
| 129 | { key: "home", label: "Home", to: `/${slug}/-/home`, icon: <House size={21} /> }, | |
| Code sits second in the rail and on a phone's tab bar: for members who have it, it is the everyday mode | 130 | code |
| 131 | ? { key: "code", label: "Code", to: `/${slug}/-/projects`, icon: <Code2 size={21} /> } | |
| 132 | : { key: "docs", label: "Docs", to: `/${slug}/-/docs`, icon: <BookOpen size={21} /> }, | |
| 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) | 133 | { |
| 134 | key: "chat", | |
| 135 | label: "Chat", | |
| 136 | to: `/${slug}/-/chat`, | |
| 137 | icon: <MessagesSquare size={21} />, | |
| 138 | badge: { count: unread.mentions > 0 ? unread.mentions : unread.chat, loud: unread.mentions > 0 }, | |
| 139 | }, | |
| 140 | { key: "agents", label: "Agents", to: `/${slug}/-/agents`, icon: <Sparkles size={21} /> }, | |
| 141 | { key: "inbox", label: "Inbox", to: "/inbox", icon: <Inbox size={21} />, badge: { count: unread.inbox, loud: true } }, | |
| 142 | ]; | |
| 143 | if (isConversation(pathname)) return null; | |
| 144 | return ( | |
| 145 | <nav | |
| 146 | aria-label="Tabs" | |
| 147 | className="fixed inset-x-0 bottom-0 z-40 border-t border-line bg-[#0b0b0d]/95 pb-[env(safe-area-inset-bottom)] backdrop-blur md:hidden in-data-[keyboard=open]:hidden" | |
| 148 | > | |
| 149 | <ul className="grid h-14 grid-cols-5"> | |
| 150 | {tabs.map((tab) => { | |
| 151 | const current = mode === tab.key; | |
| 152 | const count = tab.badge?.count ?? 0; | |
| 153 | return ( | |
| 154 | <li key={tab.key}> | |
| 155 | <Link | |
| 156 | to={tab.to} | |
| 157 | prefetch="intent" | |
| 158 | aria-current={current ? "page" : undefined} | |
| 159 | aria-label={count > 0 ? `${tab.label}, ${count} unread` : tab.label} | |
| 160 | className="flex h-full flex-col items-center justify-center gap-0.5 transition-colors active:bg-raised/60" | |
| 161 | > | |
| 162 | <span className={`relative flex h-7 w-12 items-center justify-center rounded-full transition-colors ${current ? "bg-[#2c2c33] text-fg" : "text-muted"}`}> | |
| 163 | {tab.icon} | |
| 164 | {count > 0 && ( | |
| 165 | <span | |
| 166 | className={`absolute -top-1 right-0.5 min-w-[1.125rem] rounded-full px-1 text-center text-[0.625rem] leading-[1.125rem] font-bold tabular-nums ring-2 ring-[#0b0b0d] ${ | |
| 167 | tab.badge?.loud ? "bg-accent text-bg" : "bg-[#3d3d45] text-fg" | |
| 168 | }`} | |
| 169 | > | |
| 170 | {count > 99 ? "99+" : count} | |
| 171 | </span> | |
| 172 | )} | |
| 173 | </span> | |
| 174 | <span className={`text-[0.6875rem] font-medium ${current ? "text-fg" : "text-faint"}`}>{tab.label}</span> | |
| 175 | </Link> | |
| 176 | </li> | |
| 177 | ); | |
| 178 | })} | |
| 179 | </ul> | |
| 180 | </nav> | |
| 181 | ); | |
| 182 | } | |
| 183 | ||
| 184 | /** | |
| 185 | * A sheet that rises from the bottom of a phone, with a handle to drag it | |
| 186 | * back down. Escape, a tap outside or a drag of more than 90px closes it. | |
| 187 | */ | |
| 188 | export function BottomSheet({ | |
| 189 | open, | |
| 190 | onOpenChange, | |
| 191 | title, | |
| 192 | children, | |
| 193 | }: { | |
| 194 | open: boolean; | |
| 195 | onOpenChange: (open: boolean) => void; | |
| 196 | title: string; | |
| 197 | children: ReactNode; | |
| 198 | }) { | |
| 199 | const [drag, setDrag] = useState(0); | |
| 200 | const from = useRef<number | null>(null); | |
| 201 | useEffect(() => { | |
| 202 | if (!open) setDrag(0); | |
| 203 | }, [open]); | |
| 204 | const handlers = { | |
| 205 | onPointerDown: (event: React.PointerEvent) => { | |
| 206 | from.current = event.clientY; | |
| 207 | (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId); | |
| 208 | }, | |
| 209 | onPointerMove: (event: React.PointerEvent) => { | |
| 210 | if (from.current == null) return; | |
| 211 | setDrag(Math.max(0, event.clientY - from.current)); | |
| 212 | }, | |
| 213 | onPointerUp: () => { | |
| 214 | if (drag > 90) onOpenChange(false); | |
| 215 | else setDrag(0); | |
| 216 | from.current = null; | |
| 217 | }, | |
| 218 | }; | |
| 219 | return ( | |
| 220 | <Primitive.Root open={open} onOpenChange={onOpenChange}> | |
| 221 | <Primitive.Portal> | |
| 222 | <Primitive.Overlay className="fixed inset-0 z-50 bg-black/60 data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out" /> | |
| 223 | <Primitive.Content | |
| 224 | aria-describedby={undefined} | |
| 225 | onOpenAutoFocus={(event) => event.preventDefault()} | |
| 226 | style={{ transform: drag ? `translateY(${drag}px)` : undefined, transition: from.current == null ? "transform 0.2s ease-out" : "none" }} | |
| 227 | className="fixed inset-x-0 bottom-0 z-50 flex max-h-[88dvh] flex-col rounded-t-2xl border-t border-line-strong bg-surface pb-[env(safe-area-inset-bottom)] text-fg shadow-2xl shadow-black/60 outline-none data-[state=open]:animate-[sheet-in-bottom_0.24s_cubic-bezier(0.16,1,0.3,1)]" | |
| 228 | > | |
| 229 | <div {...handlers} className="flex shrink-0 cursor-grab touch-none justify-center pt-2.5 pb-2" aria-hidden="true"> | |
| 230 | <span className="h-1.5 w-10 rounded-full bg-line-strong" /> | |
| 231 | </div> | |
| 232 | <Primitive.Title className="sr-only">{title}</Primitive.Title> | |
| 233 | <div className="min-h-0 grow overflow-y-auto overscroll-contain px-3 pb-3">{children}</div> | |
| 234 | </Primitive.Content> | |
| 235 | </Primitive.Portal> | |
| 236 | </Primitive.Root> | |
| 237 | ); | |
| 238 | } | |
| 239 | ||
| 240 | /** A row in a bottom sheet: 48px tall, an icon, words, and what sits at its end. */ | |
| 241 | export function SheetRow({ to, href, icon, children, end, onClick }: { to?: string; href?: string; icon: ReactNode; children: ReactNode; end?: ReactNode; onClick?: () => void }) { | |
| 242 | const className = "flex h-12 w-full items-center gap-3 rounded-xl px-3 text-left text-[0.9375rem] text-fg-soft transition-colors active:bg-raised"; | |
| 243 | const inner = ( | |
| 244 | <> | |
| 245 | <span className="flex w-6 shrink-0 justify-center text-muted [&_svg]:size-5">{icon}</span> | |
| 246 | <span className="min-w-0 grow truncate">{children}</span> | |
| 247 | {end} | |
| 248 | </> | |
| 249 | ); | |
| 250 | if (to) { | |
| 251 | return ( | |
| 252 | <Link to={to} className={className} onClick={onClick}> | |
| 253 | {inner} | |
| 254 | </Link> | |
| 255 | ); | |
| 256 | } | |
| 257 | if (href) { | |
| 258 | return ( | |
| 259 | <a href={href} className={className}> | |
| 260 | {inner} | |
| 261 | </a> | |
| 262 | ); | |
| 263 | } | |
| 264 | return ( | |
| 265 | <button type="button" onClick={onClick} className={className}> | |
| 266 | {inner} | |
| 267 | </button> | |
| 268 | ); | |
| 269 | } | |
| 270 | ||
| 271 | function SheetGroup({ title, children }: { title: string; children: ReactNode }) { | |
| 272 | return ( | |
| 273 | <section className="mt-2 border-t border-line pt-2"> | |
| 274 | <h3 className="px-3 pt-1 pb-1 text-xs font-medium text-faint">{title}</h3> | |
| 275 | {children} | |
| 276 | </section> | |
| 277 | ); | |
| 278 | } | |
| 279 | ||
| 280 | function displayName(membership: Membership): string { | |
| 281 | return membership.name?.trim() || membership.slug; | |
| 282 | } | |
| 283 | ||
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 284 | /** Your status, away and pausing notifications, as the account menu has them on a computer. */ |
| 285 | function OwnPresenceRows({ onEdit }: { onEdit: () => void }) { | |
| 286 | const me = useOwnPresence(); | |
| 287 | const now = Date.now(); | |
| 288 | const status = liveStatus(me?.status, now); | |
| 289 | const paused = dndOn(me, now); | |
| 290 | const away = me?.away_manual === true; | |
| 291 | return ( | |
| 292 | <> | |
| 293 | <SheetRow icon={status?.emoji ? <span className="text-lg leading-none">{status.emoji}</span> : <Smile />} onClick={onEdit}> | |
| 294 | {status ? status.text || "Status set" : "Set a status"} | |
| 295 | </SheetRow> | |
| 296 | <SheetRow icon={away ? <Sun /> : <Moon />} onClick={() => void setPresence({ away: !away }, { away_manual: !away, presence: away ? "active" : "away" })}> | |
| 297 | {away ? "Set yourself active" : "Set yourself away"} | |
| 298 | </SheetRow> | |
| 299 | {paused ? ( | |
| 300 | <SheetRow | |
| 301 | icon={<BellRing />} | |
| 302 | end={me?.dnd_until ? <span className="text-xs text-faint">{untilLabel(me.dnd_until, new Date(now))}</span> : null} | |
| 303 | onClick={() => void setPresence({ dnd_until: null }, { dnd_until: null })} | |
| 304 | > | |
| 305 | Resume notifications | |
| 306 | </SheetRow> | |
| 307 | ) : ( | |
| 308 | <SheetRow | |
| 309 | icon={<BellOff />} | |
| 310 | onClick={() => { | |
| 311 | const until = pauseUntil("1h", new Date()); | |
| 312 | void setPresence({ dnd_until: until }, { dnd_until: until }); | |
| 313 | }} | |
| 314 | > | |
| 315 | Pause notifications for 1 hour | |
| 316 | </SheetRow> | |
| 317 | )} | |
| 318 | </> | |
| 319 | ); | |
| 320 | } | |
| 321 | ||
| 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) | 322 | /** |
| 323 | * The phone's everything-else: the workspace avatar at the top left opens | |
| 324 | * it. Your workspaces, Docs, the workspace's own pages, help, and your | |
| 325 | * account. It replaces the menu sheet below 768px. | |
| 326 | */ | |
| 327 | export function AvatarSheetButton({ user, workspace }: { user: User; workspace: Membership }) { | |
| 328 | const [open, setOpen] = useState(false); | |
| 329 | const { pathname } = useLocation(); | |
| 330 | const submit = useSubmit(); | |
| 331 | useEffect(() => setOpen(false), [pathname]); | |
| 332 | const slug = workspace.slug; | |
| 333 | const owner = workspace.role === "owner"; | |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 334 | const [editing, setEditing] = useState(false); |
| 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) | 335 | return ( |
| 336 | <> | |
| 337 | <button | |
| 338 | type="button" | |
| 339 | aria-label={`${displayName(workspace)}: workspaces, settings and account`} | |
| 340 | onClick={() => setOpen(true)} | |
| 341 | className="-ml-1 flex size-10 shrink-0 items-center justify-center rounded-xl transition-transform active:scale-95 md:hidden" | |
| 342 | > | |
| 343 | <Avatar name={slug} image={workspace.avatar} size={32} square /> | |
| 344 | </button> | |
| 345 | <BottomSheet open={open} onOpenChange={setOpen} title="Workspaces, settings and account"> | |
| 346 | <div className="flex items-center gap-3 px-3 pt-1 pb-3"> | |
| 347 | <Avatar name={slug} image={workspace.avatar} size={44} square /> | |
| 348 | <div className="min-w-0"> | |
| 349 | <p className="truncate text-base font-semibold">{displayName(workspace)}</p> | |
| 350 | <p className="truncate font-mono text-xs text-muted">g1t.sh/{slug}</p> | |
| 351 | </div> | |
| 352 | </div> | |
| 353 | <SheetGroup title="Switch workspace"> | |
| 354 | {(user.workspaces ?? []).map((membership) => ( | |
| 355 | <SheetRow | |
| 356 | key={membership.slug} | |
| 357 | to={`/${membership.slug}/-/home`} | |
| 358 | icon={<Avatar name={membership.slug} image={membership.avatar} size={24} square />} | |
| 359 | end={membership.slug === slug ? <Check size={18} className="text-accent" /> : null} | |
| 360 | > | |
| 361 | {displayName(membership)} | |
| 362 | </SheetRow> | |
| 363 | ))} | |
| 364 | <SheetRow to="/workspaces/new" icon={<Plus />}> | |
| 365 | New workspace | |
| 366 | </SheetRow> | |
| 367 | </SheetGroup> | |
| 368 | <SheetGroup title="Workspace"> | |
| 369 | <SheetRow to={`/${slug}/-/docs`} icon={<BookOpen />}> | |
| 370 | Docs | |
| 371 | </SheetRow> | |
| 372 | <SheetRow to={`/${slug}/-/workspace`} icon={<Building2 />}> | |
| 373 | Overview | |
| 374 | </SheetRow> | |
| 375 | <SheetRow to={`/${slug}/-/people`} icon={<Users />}> | |
| 376 | People | |
| 377 | </SheetRow> | |
| 378 | <SheetRow to={`/${slug}/-/teams`} icon={<UsersRound />}> | |
| 379 | Teams | |
| 380 | </SheetRow> | |
| 381 | <SheetRow to={`/${slug}/-/usage`} icon={<BarChart3 />}> | |
| 382 | Usage and billing | |
| 383 | </SheetRow> | |
| 384 | <SheetRow to={`/${slug}/-/integrations`} icon={<Plug />}> | |
| 385 | Integrations | |
| 386 | </SheetRow> | |
| 387 | <SheetRow to={owner ? `/${slug}/-/settings` : `/${slug}/-/repositories`} icon={<Settings />}> | |
| 388 | Settings | |
| 389 | </SheetRow> | |
| 390 | </SheetGroup> | |
| 391 | <SheetGroup title="Help"> | |
| 392 | <SheetRow to="/support" icon={<LifeBuoy />}> | |
| 393 | Support | |
| 394 | </SheetRow> | |
| 395 | <SheetRow href="https://docs.g1t.sh/" icon={<BookOpen />}> | |
| 396 | Documentation | |
| 397 | </SheetRow> | |
| 398 | <SheetRow href={STATUS_URL} icon={<Activity />}> | |
| 399 | Status | |
| 400 | </SheetRow> | |
| 401 | </SheetGroup> | |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 402 | <SheetGroup title={`@${shownUsername(user)}`}> |
| 403 | <OwnPresenceRows | |
| 404 | onEdit={() => { | |
| 405 | setOpen(false); | |
| 406 | setEditing(true); | |
| 407 | }} | |
| 408 | /> | |
| 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) | 409 | <SheetRow to={`/u/${user.username}`} icon={<CircleUserRound />}> |
| 410 | Your profile | |
| 411 | </SheetRow> | |
| 412 | <SheetRow to="/settings" icon={<Settings />}> | |
| 413 | Your settings | |
| 414 | </SheetRow> | |
| 415 | <SheetRow icon={<LogOut />} onClick={() => submit(null, { method: "post", action: "/logout" })}> | |
| 416 | Sign out | |
| 417 | </SheetRow> | |
| 418 | </SheetGroup> | |
| 419 | </BottomSheet> | |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 420 | <StatusDialog open={editing} onOpenChange={setEditing} /> |
| 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) | 421 | </> |
| 422 | ); | |
| 423 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.