Skip to content

Commit

Notifications is the bell in the top bar alone: the rail no longer carries it, and the notifications guide points at the bell; a phone keeps it in the bottom bar, which has no bell. Home names who made a deployment, by username or by an agent's handle, instead of showing the account's id, and says someone when identity doesn't know the id. The Apps launcher's skeleton tiles are drawn in the line colour so they show on its raised surface.

syntaqxcommitted Parent853333fBrowse files
7 files+44−210/7 viewed
+2−2
99 request, a deployment failed, someone mentioned you. You are never told
1010 about what you did yourself.
1111
12−Open them from **Notifications** in the rail, or from the bottom bar on a
12+Open them from the bell in the top bar, or from the bottom bar on a
1313 phone. The number on it is what is unread. The bell at the right of the
1414 page's header opens the same notifications in a panel beside the page; its
1515 number is amber while something is waiting on you, red while a failure is
229229
230230 ## The Notifications page
231231
232−**Notifications** in the rail opens
232+The bell in the top bar opens
233233 [g1t.sh/notifications](https://g1t.sh/notifications): every thread, a page
234234 at a time, under the same tabs. Its sidebar chooses what you look at:
235235
+4−3
206206 <div className="grid grid-cols-4 gap-1 max-sm:grid-cols-3" aria-busy="true" aria-label="Loading apps">
207207 {Array.from({ length: 4 }, (_, n) => (
208208 <div key={n} className="flex flex-col items-center gap-2 px-1 pt-3 pb-2.5">
209− <Skeleton className="size-10 rounded-xl" />
210− <Skeleton className="h-2.5 w-12" />
211− <Skeleton className="h-2 w-8" />
209+ {/* On the launcher's raised surface the usual raised block would vanish: a line-coloured one shows. */}
210+ <Skeleton className="size-10 rounded-xl bg-line" />
211+ <Skeleton className="h-2.5 w-12 bg-line" />
212+ <Skeleton className="h-2 w-8 bg-line" />
212213 </div>
213214 ))}
214215 </div>
+3−3
200200 /**
201201 * The rail down the window's left edge: a narrow column of icons, each
202202 * named by the hint beside it. g1t's mark, which leads to Home; the
203− * built-in apps (Home, Chat, Notifications, Agents, Code and Artifacts)
204− * with what is unread on them; the apps you pinned; the Apps launcher;
203+ * built-in apps (Home, Chat, Agents, Code and Artifacts) with what is
204+ * unread on them (Notifications is the bell in the top bar, not a rail
205+ * app); the apps you pinned; the Apps launcher;
205206 * and at its foot People, Workspace and your account. The one you are in
206207 * sits on a filled square, and which that is follows the address. A
207208 * member without Code access has no Code. From 768px; a phone has the
234235 label: "Chat",
235236 badge: { count: unread.mentions > 0 ? unread.mentions : unread.chat, loud: unread.mentions > 0 },
236237 },
237− { key: "notifications", label: "Notifications", badge: { count: unread.notifications, loud: true } },
238238 { key: "agents", label: "Agents" },
239239 ...(code ? [{ key: "code" as const, label: "Code" }] : []),
240240 { key: "artifacts", label: "Artifacts" },
+3−2
2323 notifyClient,
2424 } from "@g1t/contracts";
2525
26−import { type CodePull, type CodeWork, G1T_ACTOR, type RepoRef, type SessionWork, type Span, type Spend, pullKey, spanMonths, spendIn } from "./home";
26+import { type CodePull, type CodeWork, G1T_ACTOR, type RepoRef, type SessionWork, type Span, type Spend, deployActorKey, pullKey, spanMonths, spendIn } from "./home";
2727 import { monthSpan, spentMicros } from "./spend";
2828 import { actions, agents, billing, chat, deployments, events, identity, repos, work, workspaceAgents } from "./services.server";
2929
164164 }
165165
166166 /** Every member key the digest and chat name. */
167−export function actorKeys(activity: ActivityDigest | null, chatActivity: ChatActivity | null): string[] {
167+export function actorKeys(activity: ActivityDigest | null, chatActivity: ChatActivity | null, deploys?: { projects: { deployments: { createdBy: string }[] }[] } | null): string[] {
168168 const keys = new Set<string>();
169+ for (const project of deploys?.projects ?? []) for (const deploy of project.deployments) keys.add(deployActorKey(deploy.createdBy) ?? "");
169170 for (const repo of activity?.repos ?? []) {
170171 for (const by of [...repo.pushes.by, ...repo.pushes.default_branch.by]) keys.add(by.actor);
171172 for (const list of [repo.pulls.opened, repo.pulls.merged, repo.pulls.closed, repo.issues.opened, repo.issues.closed, repo.reviews, repo.comments, repo.deployments.succeeded, repo.deployments.failed]) {
+11−3
674674 ],
675675 );
676676 const builds = landedDeploys([
677− { key: "deploy:site:d1", project: "site", kind: "production", outcome: "live", commit: "abcdef1", branch: null, at: NOW - HOUR, to: "/acme/site/deployments/d1", by: "g1t" },
678− { key: "deploy:site:d2", project: "site", kind: "preview", outcome: "live", commit: "abcdef2", branch: "fix", at: NOW - HOUR, to: "/acme/site/deployments/d2", by: "bo" },
679− { key: "deploy:site:d3", project: "site", kind: "production", outcome: "failed", commit: "abcdef3", branch: null, at: NOW - HOUR, to: "/acme/site/deployments/d3", by: "bo" },
677+ { key: "deploy:site:d1", project: "site", kind: "production", outcome: "live", commit: "abcdef1", branch: null, at: NOW - HOUR, to: "/acme/site/deployments/d1", by: "@g1t", agent: true },
678+ { key: "deploy:site:d2", project: "site", kind: "preview", outcome: "live", commit: "abcdef2", branch: "fix", at: NOW - HOUR, to: "/acme/site/deployments/d2", by: "bo", agent: false },
679+ { key: "deploy:site:d3", project: "site", kind: "production", outcome: "failed", commit: "abcdef3", branch: null, at: NOW - HOUR, to: "/acme/site/deployments/d3", by: "bo", agent: false },
680680 ]);
681681 assert.deepEqual(builds.map((change) => [change.kind, change.title, change.agent]), [["deploy", "Production of site went live", true]]);
682682 const all = landed([fromActivity, builds]);
720720 ],
721721 "acme",
722722 SPAN,
723+ { "user:bo": "bo" },
723724 );
724725 assert.deepEqual(rows.map((row) => [row.key, row.outcome]), [
725726 ["deploy:site:d1", "live"],
728729 ]);
729730 assert.equal(rows[0]!.to, "/acme/site/deployments/d1");
730731 assert.equal(rows[0]!.commit, "abcdef1");
732+ // The maker is named, never shown as an id; an agent's by its handle; an id identity doesn't know as someone.
733+ assert.equal(rows[0]!.by, "bo");
734+ assert.equal(rows[0]!.agent, false);
735+ const unknown = deploysIn([{ slug: "site", deployments: [deploy({ createdBy: "usr_zzz" })] }], "acme", SPAN);
736+ assert.equal(unknown[0]!.by, "someone");
737+ const robot = deploysIn([{ slug: "site", deployments: [deploy({ createdBy: "agt_01x" })] }], "acme", SPAN, { "user:agt_01x": "otto" });
738+ assert.deepEqual([robot[0]!.by, robot[0]!.agent], ["@otto", true]);
731739 });
732740
733741 const request = (over: Partial<InstallRequest>): InstallRequest => ({
+18−5
810810 key: `landed:${row.key}`,
811811 kind: "deploy" as const,
812812 title: `Production of ${row.project} went live`,
813− detail: `${row.commit} · by ${row.by}`,
813+ detail: `${row.commit} · by ${row.by ?? "someone"}`,
814814 to: row.to,
815815 at: row.at,
816− agent: isAgentName(row.by),
816+ agent: row.agent,
817817 }));
818818 }
819819
822822 return parts.flat().sort((a, b) => b.at - a.at || a.key.localeCompare(b.key));
823823 }
824824
825+/** A deployment's maker as a member key: an id becomes `user:<id>`, a key stays as it is. */
826+export function deployActorKey(createdBy: string | null | undefined): string | null {
827+ const by = (createdBy ?? "").trim();
828+ if (!by) return null;
829+ return /^(user|agent):/.test(by) ? by : `user:${by}`;
830+}
831+
825832 /** A build that finished in the span. */
826833 export type DeployRow = {
827834 key: string;
833840 branch: string | null;
834841 at: number;
835842 to: string;
836− by: string;
843+ /** Who made it, as a name (`@handle` for an agent); null when the deployment does not say. */
844+ by: string | null;
845+ /** Whether an agent made it. */
846+ agent: boolean;
837847 };
838848
839849 /**
841851 * and previews too. A build that went live and was replaced since still
842852 * went live. Skipped builds are left out.
843853 */
844−export function deploysIn(projects: { slug: string; deployments: Deployment[] }[], workspace: string, span: Pick<Span, "from" | "now">): DeployRow[] {
854+export function deploysIn(projects: { slug: string; deployments: Deployment[] }[], workspace: string, span: Pick<Span, "from" | "now">, names: Record<string, string> = {}, agentKeys?: ReadonlySet<string>): DeployRow[] {
845855 const rows: DeployRow[] = [];
846856 for (const project of projects) {
847857 for (const deploy of project.deployments) {
858+ // Who made it: the deployment names them by id, so it reads as a name, or as someone when identity doesn't know it.
859+ const key = deployActorKey(deploy.createdBy);
848860 const ended = deploy.finishedAt ?? null;
849861 if (!within(ended, span)) continue;
850862 const outcome = deploy.status === "failed" ? "failed" : deploy.status === "ready" || deploy.status === "replaced" || deploy.status === "down" ? "live" : null;
858870 branch: deploy.branch,
859871 at: Date.parse(ended!),
860872 to: `/${workspace}/${project.slug}/deployments/${deploy.id}`,
861− by: deploy.createdBy,
873+ by: key ? namesOf([key], names, agentKeys) : null,
874+ agent: !!key && actorKind(key, agentKeys) === "agent",
862875 });
863876 }
864877 }
+3−3
149149 const memoriesP = code ? loadMemories(viewer, slug).catch(() => null) : Promise.resolve(null);
150150 const activityP = reposP.then((owned) => loadActivity(slug, span, owned)).catch(() => null);
151151 const chatActivityP = loadChatActivity(viewer, slug, span).catch(() => null);
152− const namesP = Promise.all([activityP, chatActivityP, agentsP])
153− .then(([activity, chatActivity, agents]) => loadNames(actorKeys(activity, chatActivity), agents))
152+ const namesP = Promise.all([activityP, chatActivityP, agentsP, deploysP])
153+ .then(([activity, chatActivity, agents, deploys]) => loadNames(actorKeys(activity, chatActivity, deploys), agents))
154154 .catch(() => ({}) as Record<string, string>);
155155
156156 const work: Promise<SpanWork | null> = Promise.all([codeWorkP, sessionsP])
162162 .catch(() => null);
163163 const happened = Promise.all([codeWorkP, deploysP, memoriesP, requestsP, activityP, namesP, reposP])
164164 .then(([codeWork, deploys, memories, requests, activity, names, owned]) => {
165− const deployRows = deploys ? deploysIn(deploys.projects, slug, span) : null;
165+ const deployRows = deploys ? deploysIn(deploys.projects, slug, span, names) : null;
166166 const merges = codeWork && codeWork !== "no_access" ? landedIn(codeWork.pulls, span) : null;
167167 return {
168168 // What landed: merges, direct pushes, releases and packages, production builds; null when nothing of Code could be read.