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 | /** |
| 2 | * The notify service: live notifications, unread counts and browser push. | |
| The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were. | 3 | * One feed per person (src/feed.ts). |
| 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) | 4 | * |
| 5 | * Reached through service bindings only: `POST /rpc/<method>` with | |
| 6 | * snake_case bodies (`notifyClient` in @g1t/contracts), and `GET /live`, | |
| 7 | * the person's feed socket, which the site forwards after checking the | |
| 8 | * session (NOTIFY_VIEWER_HEADER), with the counts it read (NOTIFY_SEED_HEADER). | |
| 9 | * | |
| 10 | * Who sends what: | |
| 11 | * - chat: `deliver`, for every message (counts for each person in the | |
| 12 | * conversation, a notification for those it is for) and every read; | |
| 13 | * - events: `notify`, for every inbox item, and `set_inbox`, the count after | |
| 14 | * items arrive or are marked, naming the person by username; | |
| 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 | 15 | * - the site: `subscribe`, `unsubscribe`, `status`, `set_preferences`, |
| The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how. | 16 | * `test`, `presence` and `set_presence`, for the person signed in; and |
| 17 | * `last_visit` and `mark_visit`, when they were last on a workspace's | |
| 18 | * Home page (src/visits.ts); | |
| People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how. | 19 | * - agents and the site: `workspace_presence`, how a workspace's people |
| 20 | * show now (away, in Do Not Disturb, offline, and their statuses); | |
| 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 | 21 | * - integrations, later: `set_presence` with a status of `source` |
| 22 | * `calendar` or `integration`, for the person they act for. | |
| 23 | * | |
| 24 | * Presence: each feed works out its person's from their tabs and tells | |
| 25 | * one room per workspace (src/room.ts), which tells everyone there who is | |
| The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were. | 26 | * online. See docs.g1t.sh/guides/chat/, "Presence and status". |
| 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) | 27 | */ |
| 28 | import { | |
| 29 | NOTIFY_VIEWER_HEADER, | |
| 30 | identityClient, | |
| 31 | type FeedDelivery, | |
| 32 | type ServiceBinding, | |
| 33 | type Viewer, | |
| 34 | } from "@g1t/contracts"; | |
| 35 | ||
| 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 | 36 | import { FEED_USERNAME_HEADER, FEED_USER_ID_HEADER, Feed, type FeedEnv } from "./feed.ts"; |
| People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how. | 37 | import { onlyPeople } from "./presence.ts"; |
| The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how. | 38 | import { visitWorkspace } from "./visits.ts"; |
| 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) | 39 | |
| 40 | export { Feed } from "./feed.ts"; | |
| 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 | 41 | export { Room } from "./room.ts"; |
| 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) | 42 | |
| 43 | type Env = FeedEnv & { | |
| 44 | IDENTITY: ServiceBinding; | |
| 45 | FEEDS: DurableObjectNamespace<Feed>; | |
| 46 | }; | |
| 47 | ||
| 48 | function feed(env: Env, userId: string) { | |
| 49 | return env.FEEDS.get(env.FEEDS.idFromName(userId)); | |
| 50 | } | |
| 51 | ||
| 52 | /** A user id from the arguments, or one looked up by username. */ | |
| Agents' budget alerts and sessions waiting for approval reach the person they're for: notifications finds who to tell under target as well as at the top level. | 53 | async function userIdOf(env: Env, given: { user_id?: unknown; username?: unknown; target?: unknown }): Promise<string | null> { |
| 54 | // Who it's for, at the top level or under `target` as the agents service sends it. | |
| 55 | const target = given.target && typeof given.target === "object" ? (given.target as { user_id?: unknown; username?: unknown }) : null; | |
| 56 | const args = { user_id: given.user_id ?? target?.user_id, username: given.username ?? target?.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) | 57 | if (typeof args.user_id === "string" && args.user_id) return args.user_id; |
| 58 | if (typeof args.username !== "string" || !args.username) return null; | |
| 59 | const user = await identityClient(env.IDENTITY) | |
| 60 | .userByUsername(args.username) | |
| 61 | .catch(() => null); | |
| 62 | return user?.id ?? null; | |
| 63 | } | |
| 64 | ||
| 65 | /** The most deliveries in one call; chat sends one per person in the conversation. */ | |
| 66 | const MAX_DELIVERIES = 1000; | |
| 67 | ||
| 68 | async function answer(env: Env, method: string, args: any): Promise<Response> { | |
| People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how. | 69 | if (method === "workspace_presence") { |
| 70 | // For services: how a workspace's people show now, as its room keeps them. | |
| 71 | const slug = typeof args?.workspace === "string" ? args.workspace.trim().toLowerCase() : ""; | |
| 72 | if (!slug || !env.ROOMS) return Response.json([]); | |
| 73 | const everyone = await env.ROOMS.get(env.ROOMS.idFromName(slug)).people(); | |
| 74 | return Response.json(onlyPeople(everyone, Array.isArray(args.user_ids) ? args.user_ids : null)); | |
| 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 | if (method === "deliver") { |
| 77 | const items = (Array.isArray(args?.items) ? args.items : []).slice(0, MAX_DELIVERIES) as FeedDelivery[]; | |
| 78 | const byUser = new Map<string, FeedDelivery[]>(); | |
| 79 | for (const item of items) { | |
| 80 | if (typeof item?.user_id !== "string" || !item.user_id) continue; | |
| 81 | byUser.set(item.user_id, [...(byUser.get(item.user_id) ?? []), item]); | |
| 82 | } | |
| 83 | const results = await Promise.allSettled([...byUser].map(([id, mine]) => feed(env, id).deliver(mine))); | |
| 84 | for (const result of results) if (result.status === "rejected") console.error("notify: a delivery failed", result.reason); | |
| 85 | return Response.json({ ok: true }); | |
| 86 | } | |
| 87 | const userId = await userIdOf(env, args ?? {}); | |
| 88 | if (!userId) return Response.json({ ok: false }); | |
| 89 | const stub = feed(env, userId); | |
| 90 | switch (method) { | |
| 91 | case "notify": | |
| 92 | return Response.json(await stub.notify(args.notification)); | |
| 93 | case "set_inbox": | |
| 94 | return Response.json(await stub.setInbox(Number(args.unread))); | |
| 95 | case "subscribe": | |
| 96 | return Response.json(await stub.subscribe(args.subscription, typeof args.user_agent === "string" ? args.user_agent : null)); | |
| 97 | case "unsubscribe": | |
| 98 | return Response.json(await stub.unsubscribe(String(args.endpoint ?? ""))); | |
| 99 | case "status": | |
| 100 | return Response.json(await stub.status(typeof args.endpoint === "string" ? args.endpoint : null)); | |
| 101 | case "set_preferences": | |
| 102 | return Response.json(await stub.setPreferences(args.preferences)); | |
| The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how. | 103 | case "last_visit": |
| 104 | case "mark_visit": { | |
| 105 | const workspace = visitWorkspace(args.workspace); | |
| 106 | if (!workspace) return Response.json({ seen_at: null }); | |
| 107 | return Response.json(method === "last_visit" ? await stub.lastVisit(workspace) : await stub.markVisit(workspace, args.at)); | |
| 108 | } | |
| 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) | 109 | case "test": |
| 110 | return Response.json(await stub.test(typeof args.username === "string" ? args.username : "")); | |
| 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 | 111 | case "presence": |
| 112 | return Response.json(await stub.own({ user_id: userId, username: typeof args.username === "string" ? args.username : "" })); | |
| 113 | case "set_presence": | |
| 114 | return Response.json( | |
| 115 | await stub.setPresence({ user_id: userId, username: typeof args.username === "string" ? args.username : "" }, args.change ?? {}), | |
| 116 | ); | |
| 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 | default: |
| 118 | return new Response("Unknown method\n", { status: 404 }); | |
| 119 | } | |
| 120 | } | |
| 121 | ||
| 122 | /** | |
| 123 | * `GET /live`, upgraded: the viewer comes in NOTIFY_VIEWER_HEADER, set by | |
| 124 | * the site after checking the session; trusted only because this Worker | |
| 125 | * is reachable through service bindings alone. | |
| 126 | */ | |
| 127 | async function live(request: Request, env: Env): Promise<Response> { | |
| 128 | if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") { | |
| 129 | return new Response("Expected a WebSocket upgrade\n", { status: 426 }); | |
| 130 | } | |
| 131 | let viewer: Viewer = null; | |
| 132 | try { | |
| 133 | viewer = JSON.parse(request.headers.get(NOTIFY_VIEWER_HEADER) ?? "null") as Viewer; | |
| 134 | } catch { | |
| 135 | viewer = null; | |
| 136 | } | |
| 137 | if (!viewer?.id) return new Response("Sign in first\n", { status: 401 }); | |
| 138 | const headers = new Headers(request.headers); | |
| 139 | headers.delete(NOTIFY_VIEWER_HEADER); | |
| 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 | 140 | // Who the feed is for, so it can name them to the presence rooms. |
| 141 | headers.set(FEED_USER_ID_HEADER, viewer.id); | |
| 142 | headers.set(FEED_USERNAME_HEADER, 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) | 143 | return feed(env, viewer.id).fetch(new Request(request.url, { method: "GET", headers })); |
| 144 | } | |
| 145 | ||
| 146 | export default { | |
| 147 | async fetch(request: Request, env: Env): Promise<Response> { | |
| 148 | const url = new URL(request.url); | |
| 149 | if (request.method === "GET" && url.pathname === "/live") return live(request, env); | |
| 150 | const match = url.pathname.match(/^\/rpc\/([a-z_]+)$/); | |
| 151 | if (request.method !== "POST" || !match) return new Response("Not found\n", { status: 404 }); | |
| 152 | const args = (await request.json().catch(() => ({}))) as any; | |
| 153 | return answer(env, match[1], args); | |
| 154 | }, | |
| 155 | } satisfies ExportedHandler<Env>; | |
| 156 |
This file's history is long; its oldest lines are credited to the oldest commit read.