Home's People shows who did what as a chart, and nobody is 'someone' any more: @g1t's chat replies are stored under system:g1t, which Home took for an unnamed person. Now system:g1t, g1t's account and a workspace acting as it are one g1t row, and the card counts agents beside people instead of leaving them out. Each person or agent is a row (agents wear the agent marker) with a bar split by kind of work: pushes, pull requests, issues, shipped, docs, chat. A hover on a segment names it. Under the bars a legend gives each kind's totals in words and links to where they are listed; it replaces the bullet lines. The badge reads '2 people · 1 agent'. The summary sentence above still counts people only. A name that can't be read shows as 'unknown account'. Chart colours are new --color-series-1..6 tokens in app.css, a fixed categorical order checked for colour-blind separation on both surfaces. The landing page's Home tour gets the same People card between What changed and Landed (jordan, alex, claude, g1t). Checked: apps/web tsc -b, npm test (989; new: g1t is one row whichever key, unknown account), the tour's card rendered in a browser. The signed-in Home still needs a look with real data.
- G1t-Session:
- ses_01m5075p3weck8yhgj6kdx5nqb
6 files+356−1720/6 viewed
| 59 | 59 | --color-warn: light-dark(var(--g1t-light-warn), var(--g1t-warn)); | |
| 60 | 60 | --color-danger: light-dark(var(--g1t-light-danger), var(--g1t-danger)); | |
| 61 | 61 | ||
| 62 | + | /* Charts' categorical slots, in a fixed order, each stepped for its surface (validated: dataviz palette). */ | |
| 63 | + | --color-series-1: light-dark(#2a78d6, #3987e5); | |
| 64 | + | --color-series-2: light-dark(#eb6834, #d95926); | |
| 65 | + | --color-series-3: light-dark(#1baf7a, #199e70); | |
| 66 | + | --color-series-4: light-dark(#eda100, #c98500); | |
| 67 | + | --color-series-5: light-dark(#e87ba4, #d55181); | |
| 68 | + | --color-series-6: light-dark(#008300, #008300); | |
| 69 | + | ||
| 62 | 70 | --animate-fade-up: fade-up 0.6s ease-out both; | |
| 63 | 71 | /* A row that arrived on a live page (Home): in from above, lit for a moment. */ | |
| 64 | 72 | --animate-arrive: g1t-arrive 0.42s cubic-bezier(0.16, 1, 0.3, 1), g1t-fresh 2s ease-out; |
| 36 | 36 | type Digest, | |
| 37 | 37 | type DigestActor, | |
| 38 | 38 | type DigestColumn, | |
| 39 | + | type WorkKind, | |
| 40 | + | WORK_KINDS, | |
| 41 | + | WORK_LABEL, | |
| 39 | 42 | type LandedChange, | |
| 40 | 43 | type RunningRow, | |
| 41 | 44 | type ShipInfo, | |
| ⋯ | |||
| 47 | 50 | explainQuestion, | |
| 48 | 51 | landedByDay, | |
| 49 | 52 | listOf, | |
| 53 | + | plural, | |
| 50 | 54 | shippedLabel, | |
| 51 | 55 | timeOfDay, | |
| 52 | 56 | waited, | |
| ⋯ | |||
| 312 | 316 | return span.key === "last" && span.note == null ? "since you were last here" : span.words.charAt(0).toLowerCase() + span.words.slice(1); | |
| 313 | 317 | } | |
| 314 | 318 | ||
| 315 | − | /** The faces of the most active, with a tooltip naming each, and how many more there were. */ | |
| 316 | − | const FACES = 5; | |
| 319 | + | /** Each kind's colour: the chart's categorical slots, in WORK_KINDS order (app.css --color-series-*). */ | |
| 320 | + | const WORK_FILL: Record<WorkKind, string> = { | |
| 321 | + | pushes: "bg-series-1", | |
| 322 | + | pulls: "bg-series-2", | |
| 323 | + | issues: "bg-series-3", | |
| 324 | + | shipped: "bg-series-4", | |
| 325 | + | docs: "bg-series-5", | |
| 326 | + | chat: "bg-series-6", | |
| 327 | + | }; | |
| 317 | 328 | ||
| 318 | − | function Faces({ actors }: { actors: DigestActor[] }) { | |
| 319 | − | if (actors.length === 0) return null; | |
| 320 | − | const shown = actors.slice(0, FACES); | |
| 321 | − | const more = actors.length - shown.length; | |
| 322 | − | return ( | |
| 323 | − | <div className="flex items-center gap-1.5"> | |
| 324 | − | <ul className="flex -space-x-1.5" aria-label="Most active"> | |
| 325 | − | {shown.map((actor) => ( | |
| 326 | − | <li key={actor.key} className="rounded-full ring-2 ring-surface"> | |
| 327 | − | <Tooltip> | |
| 328 | − | <TooltipTrigger asChild> | |
| 329 | − | <span className="block"> | |
| 330 | − | <Avatar name={actor.name} size={22} /> | |
| 331 | − | </span> | |
| 332 | − | </TooltipTrigger> | |
| 333 | − | <TooltipContent> | |
| 334 | − | {actor.name} · {actor.count.toLocaleString("en-US")} {actor.count === 1 ? "thing" : "things"} | |
| 335 | − | </TooltipContent> | |
| 336 | − | </Tooltip> | |
| 337 | − | </li> | |
| 338 | − | ))} | |
| 339 | − | </ul> | |
| 340 | − | {more > 0 && <span className="text-xs text-muted">+{more.toLocaleString("en-US")}</span>} | |
| 341 | − | </div> | |
| 342 | − | ); | |
| 329 | + | /** The most rows the chart shows; the rest are counted under it. */ | |
| 330 | + | const WHO_ROWS = 6; | |
| 331 | + | ||
| 332 | + | /** What one actor's bar says, for a screen reader and the tooltip's lead. */ | |
| 333 | + | function actorWords(actor: DigestActor): string { | |
| 334 | + | const parts = WORK_KINDS.filter((kind) => actor.parts[kind]).map((kind) => `${WORK_LABEL[kind].toLowerCase()} ${actor.parts[kind]!.toLocaleString("en-US")}`); | |
| 335 | + | return `${actor.name}${actor.kind === "agent" ? " (agent)" : ""}: ${parts.join(", ")}`; | |
| 343 | 336 | } | |
| 344 | 337 | ||
| 345 | − | /** The lines: each what was done, with its count in the words, linked to where it is listed. */ | |
| 346 | − | function Lines({ column }: { column: DigestColumn }) { | |
| 338 | + | /** | |
| 339 | + | * Who did what: a row per person or agent, most active first, each a bar | |
| 340 | + | * split by kind of work and as long as their share of the busiest. A | |
| 341 | + | * segment names itself on hover; the legend under it is the totals. | |
| 342 | + | */ | |
| 343 | + | function WhoChart({ column }: { column: DigestColumn }) { | |
| 344 | + | const shown = column.actors.slice(0, WHO_ROWS); | |
| 345 | + | const more = column.actors.length - shown.length; | |
| 346 | + | const top = Math.max(1, ...shown.map((actor) => actor.count)); | |
| 347 | 347 | return ( | |
| 348 | − | <ul className="mt-3 grid gap-x-6 gap-y-1.5 sm:grid-cols-2"> | |
| 349 | − | {column.lines.map((line) => ( | |
| 350 | − | <li key={line.key} className="flex items-start gap-2 text-sm text-fg"> | |
| 351 | − | <span className="mt-1.75 size-1.5 shrink-0 rounded-full bg-fg-soft/60" aria-hidden /> | |
| 352 | − | {line.to ? ( | |
| 353 | − | <Link to={line.to} className="hover:underline"> | |
| 354 | − | {line.text} | |
| 355 | − | </Link> | |
| 356 | − | ) : ( | |
| 357 | − | <span>{line.text}</span> | |
| 358 | − | )} | |
| 348 | + | <ul className="mt-4 space-y-2.5" aria-label="Who did what"> | |
| 349 | + | {shown.map((actor) => ( | |
| 350 | + | <li key={actor.key} className="grid grid-cols-[minmax(0,8.5rem)_minmax(0,1fr)_2.5rem] items-center gap-3" aria-label={actorWords(actor)}> | |
| 351 | + | <span className="flex min-w-0 items-center gap-2"> | |
| 352 | + | <Avatar name={actor.name} size={20} agent={actor.kind === "agent"} ring="var(--color-surface)" /> | |
| 353 | + | <span className="truncate text-sm text-fg">{actor.name}</span> | |
| 354 | + | </span> | |
| 355 | + | <span className="flex h-2.5 gap-0.5 overflow-hidden rounded-full" style={{ width: `${Math.max(4, (actor.count / top) * 100)}%` }} aria-hidden> | |
| 356 | + | {WORK_KINDS.filter((kind) => actor.parts[kind]).map((kind) => ( | |
| 357 | + | <Tooltip key={kind}> | |
| 358 | + | <TooltipTrigger asChild> | |
| 359 | + | <span className={cn("h-full min-w-1 transition-opacity hover:opacity-80", WORK_FILL[kind])} style={{ flexGrow: actor.parts[kind] }} /> | |
| 360 | + | </TooltipTrigger> | |
| 361 | + | <TooltipContent> | |
| 362 | + | {actor.name} · {WORK_LABEL[kind]}: {actor.parts[kind]!.toLocaleString("en-US")} | |
| 363 | + | {kind === "pushes" && actor.commits > 0 ? ` (${plural(actor.commits, "commit")})` : ""} | |
| 364 | + | </TooltipContent> | |
| 365 | + | </Tooltip> | |
| 366 | + | ))} | |
| 367 | + | </span> | |
| 368 | + | <span className="text-right text-xs text-muted tabular-nums">{actor.count.toLocaleString("en-US")}</span> | |
| 359 | 369 | </li> | |
| 360 | 370 | ))} | |
| 371 | + | {more > 0 && <li className="text-xs text-muted">and {plural(more, "more")}</li>} | |
| 372 | + | </ul> | |
| 373 | + | ); | |
| 374 | + | } | |
| 375 | + | ||
| 376 | + | /** The legend: each kind with its colour, what it was in words, linked to where it is listed. */ | |
| 377 | + | function WorkLegend({ column }: { column: DigestColumn }) { | |
| 378 | + | return ( | |
| 379 | + | <ul className="mt-4 grid gap-x-6 gap-y-1.5 border-t border-line pt-3 sm:grid-cols-2"> | |
| 380 | + | {column.lines.map((line) => { | |
| 381 | + | const body = ( | |
| 382 | + | <> | |
| 383 | + | <span className={cn("mt-1.25 size-2.5 shrink-0 rounded-[3px]", WORK_FILL[line.key])} aria-hidden /> | |
| 384 | + | <span className="min-w-0"> | |
| 385 | + | <span className="font-medium text-fg">{WORK_LABEL[line.key]}</span> <span className="text-muted">{line.text}</span> | |
| 386 | + | </span> | |
| 387 | + | </> | |
| 388 | + | ); | |
| 389 | + | return ( | |
| 390 | + | <li key={line.key} className="text-sm"> | |
| 391 | + | {line.to ? ( | |
| 392 | + | <Link to={line.to} className="group flex items-start gap-2 hover:[&_.text-muted]:text-fg-soft"> | |
| 393 | + | {body} | |
| 394 | + | </Link> | |
| 395 | + | ) : ( | |
| 396 | + | <span className="flex items-start gap-2">{body}</span> | |
| 397 | + | )} | |
| 398 | + | </li> | |
| 399 | + | ); | |
| 400 | + | })} | |
| 361 | 401 | </ul> | |
| 362 | 402 | ); | |
| 363 | 403 | } | |
| 364 | 404 | ||
| 405 | + | /** "2 people · 1 agent". */ | |
| 406 | + | function whoCount(actors: DigestActor[]): string { | |
| 407 | + | const people = actors.filter((actor) => actor.kind === "person").length; | |
| 408 | + | const agents = actors.length - people; | |
| 409 | + | return [people > 0 ? plural(people, "person", "people") : null, agents > 0 ? plural(agents, "agent") : null].filter(Boolean).join(" · "); | |
| 410 | + | } | |
| 411 | + | ||
| 365 | 412 | /** | |
| 366 | 413 | * What people did in the span, the top of the span's card: lines with | |
| 367 | 414 | * counts linked to where each thing is listed, and the most active faces. | |
| ⋯ | |||
| 387 | 434 | </h3> | |
| 388 | 435 | {col.actors.length > 0 && ( | |
| 389 | 436 | <Badge tone="neutral" size="md" className="tabular-nums"> | |
| 390 | − | {col.actors.length.toLocaleString("en-US")} active | |
| 437 | + | {whoCount(col.actors)} | |
| 391 | 438 | </Badge> | |
| 392 | 439 | )} | |
| 393 | − | <span className="ml-auto"> | |
| 394 | − | <Faces actors={col.actors} /> | |
| 395 | − | </span> | |
| 396 | 440 | </div> | |
| 397 | 441 | {col.lines.length > 0 ? ( | |
| 398 | − | <Lines column={col} /> | |
| 442 | + | <> | |
| 443 | + | {col.actors.length > 0 && <WhoChart column={col} />} | |
| 444 | + | <WorkLegend column={col} /> | |
| 445 | + | </> | |
| 399 | 446 | ) : ( | |
| 400 | 447 | <p className="mt-3 text-sm text-muted"> | |
| 401 | 448 | {silent.length > 0 | |
| 402 | 449 | ? `Nothing from what could be read ${when}.` | |
| 403 | 450 | : code | |
| 404 | − | ? `Nobody pushed, opened, merged, reviewed, edited a doc or said anything ${when}.` | |
| 405 | − | : `Nobody edited a doc or said anything ${when}.`} | |
| 451 | + | ? `Nothing pushed, opened, merged, reviewed, edited or said ${when}.` | |
| 452 | + | : `No doc edited and nothing said ${when}.`} | |
| 406 | 453 | </p> | |
| 407 | 454 | )} | |
| 408 | 455 | {silent.length > 0 && ( | |
| 70 | 70 | import { codingAgentById } from "@g1t/contracts/coding-agents"; | |
| 71 | 71 | ||
| 72 | 72 | import { cn } from "../lib/cn"; | |
| 73 | + | import { WORK_KINDS, WORK_LABEL, type WorkKind } from "../lib/home"; | |
| 73 | 74 | import { CHECKS, type CursorTarget, type Frame, LOOP_MS, PILLS, SESSION, SETTLED, SUMMARY_WORDS, type SessionEntry, STILLS, type Scene, frameAt, pillAt, pillProgress, sameFrame } from "../lib/tour"; | |
| 74 | 75 | import { AgentBadge, AgentMark } from "./coding-agent"; | |
| 75 | 76 | import { MarkGlyph } from "./logo"; | |
| ⋯ | |||
| 957 | 958 | ); | |
| 958 | 959 | } | |
| 959 | 960 | ||
| 961 | + | /** The chart's colour for each kind, as components/home.tsx gives it. */ | |
| 962 | + | const TOUR_FILL: Record<WorkKind, string> = { | |
| 963 | + | pushes: "bg-series-1", | |
| 964 | + | pulls: "bg-series-2", | |
| 965 | + | issues: "bg-series-3", | |
| 966 | + | shipped: "bg-series-4", | |
| 967 | + | docs: "bg-series-5", | |
| 968 | + | chat: "bg-series-6", | |
| 969 | + | }; | |
| 970 | + | ||
| 971 | + | /** | |
| 972 | + | * People: who did what overnight as the real card draws it (components/home.tsx | |
| 973 | + | * WhoChart): a row each for people and agents, a bar split by kind of work, | |
| 974 | + | * and the legend with the totals. | |
| 975 | + | */ | |
| 976 | + | function PeopleCardMock() { | |
| 977 | + | const rows: { name: string; agent: boolean; parts: Partial<Record<WorkKind, number>> }[] = [ | |
| 978 | + | { name: "jordan", agent: false, parts: { pushes: 4, pulls: 2, issues: 3, chat: 9 } }, | |
| 979 | + | { name: "alex", agent: false, parts: { pulls: 3, issues: 2, docs: 2, chat: 6 } }, | |
| 980 | + | { name: "claude", agent: true, parts: { pushes: 6, pulls: 2 } }, | |
| 981 | + | { name: "g1t", agent: true, parts: { shipped: 3, chat: 4 } }, | |
| 982 | + | ]; | |
| 983 | + | const legend: [WorkKind, string][] = [ | |
| 984 | + | ["pushes", "23 commits to 2 projects"], | |
| 985 | + | ["pulls", "4 opened, 3 merged"], | |
| 986 | + | ["issues", "3 opened, 2 closed"], | |
| 987 | + | ["shipped", "3 deploys"], | |
| 988 | + | ["docs", "2 docs edited"], | |
| 989 | + | ["chat", "19 messages in 4 conversations"], | |
| 990 | + | ]; | |
| 991 | + | const total = (parts: Partial<Record<WorkKind, number>>) => Object.values(parts).reduce((a, b) => a + b, 0); | |
| 992 | + | const top = Math.max(...rows.map((row) => total(row.parts))); | |
| 993 | + | return ( | |
| 994 | + | <HomeCard title="People" hint="2 people · 2 agents"> | |
| 995 | + | <ul className="space-y-2 px-4 pb-3"> | |
| 996 | + | {rows.map((row) => ( | |
| 997 | + | <li key={row.name} className="grid grid-cols-[5.5rem_minmax(0,1fr)_1.5rem] items-center gap-2.5"> | |
| 998 | + | <span className="flex min-w-0 items-center gap-1.5"> | |
| 999 | + | <Avatar name={row.name} size={16} agent={row.agent} ring="var(--color-surface)" /> | |
| 1000 | + | <span className="truncate text-[11.5px] text-fg">{row.name}</span> | |
| 1001 | + | </span> | |
| 1002 | + | <span className="flex h-2 gap-0.5 overflow-hidden rounded-full" style={{ width: `${(total(row.parts) / top) * 100}%` }}> | |
| 1003 | + | {WORK_KINDS.filter((kind) => row.parts[kind]).map((kind) => ( | |
| 1004 | + | <span key={kind} className={cn("h-full", TOUR_FILL[kind])} style={{ flexGrow: row.parts[kind] }} /> | |
| 1005 | + | ))} | |
| 1006 | + | </span> | |
| 1007 | + | <span className="text-right text-[10.5px] text-muted tabular-nums">{total(row.parts)}</span> | |
| 1008 | + | </li> | |
| 1009 | + | ))} | |
| 1010 | + | </ul> | |
| 1011 | + | <ul className="grid grid-cols-2 gap-x-4 gap-y-1 border-t border-line px-4 py-2.5"> | |
| 1012 | + | {legend.map(([kind, text]) => ( | |
| 1013 | + | <li key={kind} className="flex min-w-0 items-center gap-1.5 text-[10.5px]"> | |
| 1014 | + | <span className={cn("size-2 shrink-0 rounded-[2px]", TOUR_FILL[kind])} /> | |
| 1015 | + | <span className="truncate"> | |
| 1016 | + | <span className="font-medium text-fg">{WORK_LABEL[kind]}</span> <span className="text-muted">{text}</span> | |
| 1017 | + | </span> | |
| 1018 | + | </li> | |
| 1019 | + | ))} | |
| 1020 | + | </ul> | |
| 1021 | + | </HomeCard> | |
| 1022 | + | ); | |
| 1023 | + | } | |
| 1024 | + | ||
| 960 | 1025 | /** Landed: each change that came in overnight with whether it is live, the agent's mark on the one it helped make, and Explain. */ | |
| 961 | 1026 | function LandedCardMock() { | |
| 962 | 1027 | const rows = [ | |
| ⋯ | |||
| 994 | 1059 | /** | |
| 995 | 1060 | * Home, as Priya starts her day on it (routes/workspace/home.tsx): "What are | |
| 996 | 1061 | * we building today?" over the box that asks g1t, Where you're needed | |
| 997 | − | * beside Start here, then Since you were last here with What changed and | |
| 998 | − | * Landed. Pressing the first starter brings What changed into view, as the | |
| 1062 | + | * beside Start here, then Since you were last here with What changed, | |
| 1063 | + | * People (who did what, agents beside people) and Landed. Pressing the first starter brings What changed into view, as the | |
| 999 | 1064 | * real one does, and its answer streams in. | |
| 1000 | 1065 | */ | |
| 1001 | 1066 | function HomeMain({ frame, phone = false }: { frame: Frame; phone?: boolean }) { | |
| ⋯ | |||
| 1061 | 1126 | <p className="truncate text-[11px] text-muted">Yesterday at 6:12 PM · 15 hours</p> | |
| 1062 | 1127 | </div> | |
| 1063 | 1128 | <WhatChangedCardMock frame={frame} /> | |
| 1129 | + | <PeopleCardMock /> | |
| 1064 | 1130 | <LandedCardMock /> | |
| 1065 | 1131 | </div> | |
| 1066 | 1132 | </div> | |
| 21 | 21 | type User, | |
| 22 | 22 | type WorkflowRun, | |
| 23 | 23 | notifyClient, | |
| 24 | + | G1T_KEY, | |
| 24 | 25 | } from "@g1t/contracts"; | |
| 25 | 26 | ||
| 26 | 27 | import { | |
| ⋯ | |||
| 194 | 195 | const names: Record<string, string> = {}; | |
| 195 | 196 | const userIds: string[] = []; | |
| 196 | 197 | for (const key of keys.slice(0, MAX_NAMES)) { | |
| 197 | − | if (key === G1T_ACTOR || key === "user:g1t" || key.startsWith("user:wsp_")) names[key] = "g1t"; | |
| 198 | + | if (key === G1T_ACTOR || key === "user:g1t" || key === G1T_KEY || key.startsWith("user:wsp_")) names[key] = "g1t"; | |
| 198 | 199 | else if (key.startsWith("agent:") || key.startsWith("user:agt_")) continue; | |
| 199 | 200 | else if (key.startsWith("user:")) userIds.push(key.slice("user:".length)); | |
| 200 | 201 | } | |
| 393 | 393 | assert.equal(actorKind("user:usr_bot", new Set(["user:usr_bot"])), "agent"); | |
| 394 | 394 | }); | |
| 395 | 395 | ||
| 396 | − | test("the digest: what people did, in lines with counts, and who", () => { | |
| 396 | + | test("the digest: what was done, by kind with counts, and who, agents marked", () => { | |
| 397 | 397 | const digest = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: busyChat, names: NAMES, code: true }); | |
| 398 | 398 | assert.deepEqual( | |
| 399 | − | digest.people.lines.map((line) => [line.text, line.to]), | |
| 399 | + | digest.people.lines.map((line) => [line.key, line.text, line.to]), | |
| 400 | 400 | [ | |
| 401 | − | ["3 people pushed 41 commits to 4 projects", "/acme/-/repositories"], | |
| 402 | − | ["opened 2 pull requests, merged 5", "/acme/-/repositories"], | |
| 403 | − | ["reviewed 2 pull requests", "/acme/web/pulls"], | |
| 404 | − | ["opened 2 issues, closed 1", "/acme/web/issues"], | |
| 405 | − | ["left 4 comments", "/acme/web/issues"], | |
| 406 | − | ["2 deploys went out", "/acme/web/deployments"], | |
| 407 | − | ["published 1 release", "/acme/web/releases"], | |
| 408 | − | ["created 1 doc, edited 6 docs", "/acme/-/artifacts"], | |
| 409 | − | ["sent 105 messages in 5 conversations", "/acme/-/chat"], | |
| 401 | + | ["pushes", "47 commits to 4 projects", "/acme/-/repositories"], | |
| 402 | + | ["pulls", "5 opened, 5 merged, 4 reviewed", "/acme/-/repositories"], | |
| 403 | + | ["issues", "2 opened, 2 closed, 7 comments", "/acme/web/issues"], | |
| 404 | + | ["shipped", "2 deploys, 1 release, 1 package version", "/acme/web/deployments"], | |
| 405 | + | ["docs", "1 doc created, 7 docs edited", "/acme/-/artifacts"], | |
| 406 | + | ["chat", "120 messages in 5 conversations", "/acme/-/chat"], | |
| 410 | 407 | ], | |
| 411 | 408 | ); | |
| 412 | − | // Faces: most active first, by name. | |
| 409 | + | // Rows: most active first, people and agents, each marked; nobody is "someone". | |
| 413 | 410 | assert.deepEqual( | |
| 414 | − | digest.people.actors.map((actor) => actor.name), | |
| 415 | − | ["ana", "bo", "cy"], | |
| 411 | + | digest.people.actors.map((actor) => [actor.name, actor.kind]), | |
| 412 | + | digest.people.actors.map((actor) => [actor.name, actor.name === "margo" || actor.name === "g1t" ? "agent" : "person"]), | |
| 416 | 413 | ); | |
| 414 | + | assert.deepEqual(new Set(digest.people.actors.map((actor) => actor.name)), new Set(["ana", "bo", "cy", "margo", "g1t"])); | |
| 415 | + | assert.equal(digest.people.actors[0]!.name, "ana"); | |
| 416 | + | // Each row's parts add up to its count, and the kinds' counts to every row's. | |
| 417 | + | for (const actor of digest.people.actors) { | |
| 418 | + | assert.equal(Object.values(actor.parts).reduce((a, b) => a + b, 0), actor.count, actor.name); | |
| 419 | + | } | |
| 420 | + | // The summary still counts people only. | |
| 417 | 421 | assert.deepEqual(digest.summary, { people: 3, commits: 41, merged: 5, said: 105, anything: true, peopleRead: true }); | |
| 418 | 422 | assert.deepEqual(digest.missing, []); | |
| 419 | 423 | assert.equal(digest.partial, false); | |
| 420 | 424 | }); | |
| 421 | 425 | ||
| 426 | + | test("g1t is one row, named, whichever key it used: its account, a workspace, or chat's system:g1t", () => { | |
| 427 | + | const chat: ChatActivity = { ...busyChat, messages: 5, channels: 1, authors: [{ key: "system:g1t", messages: 3 }, { key: ANA, messages: 2 }] }; | |
| 428 | + | const activity = { ...busyDay(), folios: null, repos: [{ ...quietRepo("rep_web"), comments: [count(G1T, 2), count("user:wsp_acme", 1)] }] }; | |
| 429 | + | const digest = digestOf({ slug: "acme", repos: REPOS, activity, chat, names: { [ANA]: "ana", "user:wsp_acme": "g1t", "system:g1t": "g1t" }, code: true }); | |
| 430 | + | assert.deepEqual( | |
| 431 | + | digest.people.actors.map((actor) => [actor.name, actor.kind, actor.count, actor.parts]), | |
| 432 | + | [ | |
| 433 | + | ["g1t", "agent", 6, { issues: 3, chat: 3 }], | |
| 434 | + | ["ana", "person", 2, { chat: 2 }], | |
| 435 | + | ], | |
| 436 | + | ); | |
| 437 | + | assert.equal(digest.summary.people, 1); | |
| 438 | + | // A person whose name could not be read is an unknown account, never "someone". | |
| 439 | + | const unread = digestOf({ slug: "acme", repos: REPOS, activity: null, chat: { ...chat, authors: [{ key: "user:usr_gone", messages: 1 }] }, names: {}, code: true }); | |
| 440 | + | assert.deepEqual(unread.people.actors.map((actor) => actor.name), ["unknown account"]); | |
| 441 | + | }); | |
| 442 | + | ||
| 422 | 443 | test("the digest says which part didn't answer, and counts what could be read", () => { | |
| 423 | 444 | const noChat = digestOf({ slug: "acme", repos: REPOS, activity: busyDay(), chat: null, names: NAMES, code: true }); | |
| 424 | 445 | assert.deepEqual(noChat.missing, ["Chat"]); | |
| ⋯ | |||
| 427 | 448 | assert.deepEqual(noCode.missing, ["Code"]); | |
| 428 | 449 | assert.deepEqual( | |
| 429 | 450 | noCode.people.lines.map((line) => line.text), | |
| 430 | − | ["sent 105 messages in 5 conversations"], | |
| 451 | + | ["120 messages in 5 conversations"], | |
| 431 | 452 | ); | |
| 432 | 453 | assert.equal(noCode.summary.peopleRead, true); | |
| 433 | 454 | // Without Code access, events are read for artifacts only: no repositories, nothing missing. | |
| ⋯ | |||
| 451 | 472 | }); | |
| 452 | 473 | assert.deepEqual( | |
| 453 | 474 | imported.people.lines.map((line) => [line.text, line.to]), | |
| 454 | − | [["7 commits pushed to 1 project", "/acme/web/activity"]], | |
| 475 | + | [["7 commits to 1 project", "/acme/web/activity"]], | |
| 455 | 476 | ); | |
| 456 | 477 | assert.equal(imported.summary.people, 0); | |
| 457 | 478 | }); | |
| 103 | 103 | note: "first" | "capped" | "unknown" | null; | |
| 104 | 104 | }; | |
| 105 | 105 | ||
| 106 | − | const plural = (n: number, one: string, many = `${one}s`) => `${n.toLocaleString("en-US")} ${n === 1 ? one : many}`; | |
| 106 | + | export const plural = (n: number, one: string, many = `${one}s`) => `${n.toLocaleString("en-US")} ${n === 1 ? one : many}`; | |
| 107 | 107 | ||
| 108 | 108 | /** How long a stretch of time is, roughly: "a moment", "40 minutes", "3 hours", "3 days", "2 weeks". */ | |
| 109 | 109 | export function duration(ms: number): string { | |
| ⋯ | |||
| 213 | 213 | export const G1T_ACTOR = "user:usr_g1t_agent"; | |
| 214 | 214 | ||
| 215 | 215 | export function actorKind(key: string, agents?: ReadonlySet<string>): ActorKind { | |
| 216 | − | return key.startsWith("agent:") || key === G1T_ACTOR || isRobotKey(key) || agents?.has(key) ? "agent" : "person"; | |
| 216 | + | return key.startsWith("agent:") || key.startsWith("system:") || key === G1T_ACTOR || isRobotKey(key) || agents?.has(key) ? "agent" : "person"; | |
| 217 | 217 | } | |
| 218 | 218 | ||
| 219 | 219 | /** | |
| ⋯ | |||
| 233 | 233 | /** A repository as the digest's links and lines need it. */ | |
| 234 | 234 | export type RepoRef = RepoPath & { defaultBranch: string }; | |
| 235 | 235 | ||
| 236 | − | /** Someone who did something in the span: for the faces beside the card. */ | |
| 236 | + | /** | |
| 237 | + | * The kinds of work the span's chart is split into, in the order the | |
| 238 | + | * legend and each bar show them (and the chart's colour slots). | |
| 239 | + | */ | |
| 240 | + | export const WORK_KINDS = ["pushes", "pulls", "issues", "shipped", "docs", "chat"] as const; | |
| 241 | + | export type WorkKind = (typeof WORK_KINDS)[number]; | |
| 242 | + | ||
| 243 | + | /** Each kind's name in the legend. */ | |
| 244 | + | export const WORK_LABEL: Record<WorkKind, string> = { | |
| 245 | + | pushes: "Pushes", | |
| 246 | + | pulls: "Pull requests", | |
| 247 | + | issues: "Issues", | |
| 248 | + | shipped: "Shipped", | |
| 249 | + | docs: "Docs", | |
| 250 | + | chat: "Chat", | |
| 251 | + | }; | |
| 252 | + | ||
| 253 | + | /** A person, or an agent, who did something in the span: a row of the chart. */ | |
| 237 | 254 | export type DigestActor = { | |
| 238 | 255 | key: string; | |
| 239 | − | /** Their username; `someone` when it could not be read. */ | |
| 256 | + | /** Their username; g1t for g1t; `unknown account` when it could not be read. */ | |
| 240 | 257 | name: string; | |
| 241 | − | /** How many things they did, over every line: the order of the faces. */ | |
| 258 | + | kind: ActorKind; | |
| 259 | + | /** How many things they did, over every kind: the order of the rows. */ | |
| 242 | 260 | count: number; | |
| 261 | + | /** The same things by kind (a push counts once, however many commits). */ | |
| 262 | + | parts: Partial<Record<WorkKind, number>>; | |
| 263 | + | /** Commits in their pushes. */ | |
| 264 | + | commits: number; | |
| 243 | 265 | }; | |
| 244 | 266 | ||
| 245 | − | /** One line: what was done, with its count, and where that is listed. */ | |
| 267 | + | /** One kind of work: what was done, with its count, and where that is listed. */ | |
| 246 | 268 | export type DigestLine = { | |
| 247 | − | key: string; | |
| 269 | + | key: WorkKind; | |
| 248 | 270 | text: string; | |
| 271 | + | /** Things of this kind, in the bars' units. */ | |
| 272 | + | count: number; | |
| 249 | 273 | to: string | null; | |
| 250 | 274 | }; | |
| 251 | 275 | ||
| 252 | 276 | export type DigestColumn = { | |
| 253 | 277 | lines: DigestLine[]; | |
| 254 | − | /** Most active first. */ | |
| 278 | + | /** Most active first: people and agents, each marked. */ | |
| 255 | 279 | actors: DigestActor[]; | |
| 256 | 280 | }; | |
| 257 | 281 | ||
| 258 | 282 | /** What the sentence under the heading needs. */ | |
| 259 | 283 | export type DigestSummary = { | |
| 260 | − | /** Distinct people who did anything. */ | |
| 284 | + | /** Distinct people (not agents) who did anything. */ | |
| 261 | 285 | people: number; | |
| 262 | 286 | /** Commits people pushed. */ | |
| 263 | 287 | commits: number; | |
| ⋯ | |||
| 319 | 343 | return only ? `/${only.namespace}/${only.name}/${page}` : `/${slug}/-/repositories`; | |
| 320 | 344 | } | |
| 321 | 345 | ||
| 346 | + | /** `3 opened, 5 merged`: the non-zero counts, `#` standing for each. */ | |
| 347 | + | function counted(parts: [number, string][]): string { | |
| 348 | + | return parts | |
| 349 | + | .filter(([count]) => count > 0) | |
| 350 | + | .map(([count, words]) => words.replace("#", count.toLocaleString("en-US"))) | |
| 351 | + | .join(", "); | |
| 352 | + | } | |
| 353 | + | ||
| 322 | 354 | /** | |
| 323 | − | * What people did in the span, as lines with counts and links, and who | |
| 324 | − | * did it. Pure: everything comes from the services' counts and the names | |
| 355 | + | * What was done in the span, by kind, with counts and links, and who did | |
| 356 | + | * it: people and agents both, each marked, so g1t and the agents show | |
| 357 | + | * beside the people instead of being left out (or showing as nobody). | |
| 358 | + | * g1t's keys (its account, its chat key, a workspace acting as it) are one | |
| 359 | + | * row. Pure: everything comes from the services' counts and the names | |
| 325 | 360 | * looked up for them. | |
| 326 | 361 | */ | |
| 327 | 362 | export function digestColumn(input: DigestInput): DigestColumn { | |
| 328 | 363 | const { slug, repos, activity, chat, names, agentKeys } = input; | |
| 329 | 364 | const lines: DigestLine[] = []; | |
| 330 | − | const did = new Map<string, number>(); | |
| 331 | − | const credit = (actors: Map<string, number>) => { | |
| 332 | − | for (const [key, n] of actors) did.set(key, (did.get(key) ?? 0) + n); | |
| 365 | + | const rows = new Map<string, DigestActor>(); | |
| 366 | + | const row = (key: string): DigestActor | null => { | |
| 367 | + | if (!key) return null; | |
| 368 | + | const kind = actorKind(key, agentKeys); | |
| 369 | + | const name = names[key] ?? (kind === "agent" ? (key.endsWith(":g1t") || key === G1T_ACTOR ? "g1t" : "agent") : "unknown account"); | |
| 370 | + | const id = name === "g1t" ? "g1t" : key; | |
| 371 | + | let found = rows.get(id); | |
| 372 | + | if (!found) { | |
| 373 | + | found = { key: id, name, kind, count: 0, parts: {}, commits: 0 }; | |
| 374 | + | rows.set(id, found); | |
| 375 | + | } | |
| 376 | + | return found; | |
| 377 | + | }; | |
| 378 | + | /** Credits each actor with their part of a tally; the tally's total. */ | |
| 379 | + | const credit = (kind: WorkKind, counts: ActorCount[]) => { | |
| 380 | + | let total = 0; | |
| 381 | + | for (const { actor, count } of counts) { | |
| 382 | + | total += count; | |
| 383 | + | const who = row(actor); | |
| 384 | + | if (!who) continue; | |
| 385 | + | who.count += count; | |
| 386 | + | who.parts[kind] = (who.parts[kind] ?? 0) + count; | |
| 387 | + | } | |
| 388 | + | return total; | |
| 333 | 389 | }; | |
| 334 | 390 | ||
| 335 | 391 | if (activity) { | |
| 336 | 392 | // Pushes: commits, projects, and who. | |
| 337 | − | const pushers = new Map<string, number>(); | |
| 338 | 393 | const pushed = new Set<string>(); | |
| 339 | 394 | let commits = 0; | |
| 340 | 395 | let pushes = 0; | |
| 341 | 396 | for (const repo of activity.repos) { | |
| 342 | − | let any = false; | |
| 343 | 397 | for (const by of repo.pushes.by) { | |
| 344 | − | if (actorKind(by.actor, agentKeys) !== "person") continue; | |
| 345 | − | any = true; | |
| 398 | + | if (by.count === 0) continue; | |
| 399 | + | pushed.add(repo.repo_id); | |
| 346 | 400 | commits += by.commits; | |
| 347 | − | pushes += by.count; | |
| 348 | − | if (by.actor) pushers.set(by.actor, (pushers.get(by.actor) ?? 0) + by.count); | |
| 401 | + | pushes += credit("pushes", [{ actor: by.actor, count: by.count }]); | |
| 402 | + | const who = row(by.actor); | |
| 403 | + | if (who) who.commits += by.commits; | |
| 349 | 404 | } | |
| 350 | − | if (any) pushed.add(repo.repo_id); | |
| 351 | 405 | } | |
| 352 | 406 | if (pushes > 0) { | |
| 353 | − | const where = `to ${plural(pushed.size, "project")}`; | |
| 354 | − | const text = pushers.size > 0 ? `${people(pushers.size)} pushed ${plural(commits, "commit")} ${where}` : `${plural(commits, "commit")} pushed ${where}`; | |
| 355 | − | lines.push({ key: "pushes", text, to: placeFor(slug, repos, pushed, "activity") }); | |
| 356 | − | credit(pushers); | |
| 407 | + | lines.push({ key: "pushes", text: `${plural(commits, "commit")} to ${plural(pushed.size, "project")}`, count: pushes, to: placeFor(slug, repos, pushed, "activity") }); | |
| 357 | 408 | } | |
| 358 | 409 | ||
| 359 | − | // Pull requests, reviews, issues, comments, deploys, releases, packages: summed over projects. | |
| 360 | − | const sum = (pick: (repo: RepoDigest) => ActorCount[]) => { | |
| 361 | − | const total = { count: 0, actors: new Map<string, number>(), repoIds: [] as string[] }; | |
| 410 | + | // The other kinds, summed over projects: how many, and where. | |
| 411 | + | const sum = (kind: WorkKind, pick: (repo: RepoDigest) => ActorCount[]) => { | |
| 412 | + | let count = 0; | |
| 413 | + | const repoIds: string[] = []; | |
| 362 | 414 | for (const repo of activity.repos) { | |
| 363 | − | const { count, actors } = part(pick(repo), agentKeys); | |
| 364 | − | if (count === 0) continue; | |
| 365 | − | total.count += count; | |
| 366 | − | total.repoIds.push(repo.repo_id); | |
| 367 | − | for (const [key, n] of actors) total.actors.set(key, (total.actors.get(key) ?? 0) + n); | |
| 415 | + | const n = credit(kind, pick(repo)); | |
| 416 | + | if (n === 0) continue; | |
| 417 | + | count += n; | |
| 418 | + | repoIds.push(repo.repo_id); | |
| 368 | 419 | } | |
| 369 | − | return total; | |
| 420 | + | return { count, repoIds }; | |
| 370 | 421 | }; | |
| 371 | − | const opened = sum((repo) => repo.pulls.opened); | |
| 372 | − | const merged = sum((repo) => repo.pulls.merged); | |
| 373 | − | const closed = sum((repo) => repo.pulls.closed); | |
| 374 | − | if (opened.count + merged.count + closed.count > 0) { | |
| 375 | − | const parts: string[] = []; | |
| 376 | − | if (opened.count > 0) parts.push(`opened ${plural(opened.count, "pull request")}`); | |
| 377 | − | if (merged.count > 0) parts.push(`merged ${opened.count > 0 ? merged.count.toLocaleString("en-US") : plural(merged.count, "pull request")}`); | |
| 378 | − | if (closed.count > 0) parts.push(`closed ${closed.count.toLocaleString("en-US")} without merging`); | |
| 379 | − | lines.push({ key: "pulls", text: parts.join(", "), to: placeFor(slug, repos, [...opened.repoIds, ...merged.repoIds, ...closed.repoIds], "pulls") }); | |
| 380 | − | credit(opened.actors); | |
| 381 | − | credit(merged.actors); | |
| 382 | − | credit(closed.actors); | |
| 383 | − | } | |
| 384 | − | const reviews = sum((repo) => repo.reviews); | |
| 385 | − | if (reviews.count > 0) { | |
| 386 | − | lines.push({ key: "reviews", text: `reviewed ${plural(reviews.count, "pull request")}`, to: placeFor(slug, repos, reviews.repoIds, "pulls") }); | |
| 387 | − | credit(reviews.actors); | |
| 422 | + | ||
| 423 | + | const opened = sum("pulls", (repo) => repo.pulls.opened); | |
| 424 | + | const merged = sum("pulls", (repo) => repo.pulls.merged); | |
| 425 | + | const closed = sum("pulls", (repo) => repo.pulls.closed); | |
| 426 | + | const reviews = sum("pulls", (repo) => repo.reviews); | |
| 427 | + | const pulls = opened.count + merged.count + closed.count + reviews.count; | |
| 428 | + | if (pulls > 0) { | |
| 429 | + | const text = counted([ | |
| 430 | + | [opened.count, "# opened"], | |
| 431 | + | [merged.count, "# merged"], | |
| 432 | + | [closed.count, "# closed without merging"], | |
| 433 | + | [reviews.count, "# reviewed"], | |
| 434 | + | ]); | |
| 435 | + | lines.push({ key: "pulls", text, count: pulls, to: placeFor(slug, repos, [...opened.repoIds, ...merged.repoIds, ...closed.repoIds, ...reviews.repoIds], "pulls") }); | |
| 388 | 436 | } | |
| 389 | − | const issuesOpened = sum((repo) => repo.issues.opened); | |
| 390 | − | const issuesClosed = sum((repo) => repo.issues.closed); | |
| 391 | − | if (issuesOpened.count + issuesClosed.count > 0) { | |
| 392 | − | const parts: string[] = []; | |
| 393 | − | if (issuesOpened.count > 0) parts.push(`opened ${plural(issuesOpened.count, "issue")}`); | |
| 394 | − | if (issuesClosed.count > 0) parts.push(`closed ${issuesOpened.count > 0 ? issuesClosed.count.toLocaleString("en-US") : plural(issuesClosed.count, "issue")}`); | |
| 395 | − | lines.push({ key: "issues", text: parts.join(", "), to: placeFor(slug, repos, [...issuesOpened.repoIds, ...issuesClosed.repoIds], "issues") }); | |
| 396 | − | credit(issuesOpened.actors); | |
| 397 | − | credit(issuesClosed.actors); | |
| 437 | + | ||
| 438 | + | const issuesOpened = sum("issues", (repo) => repo.issues.opened); | |
| 439 | + | const issuesClosed = sum("issues", (repo) => repo.issues.closed); | |
| 440 | + | const comments = sum("issues", (repo) => repo.comments); | |
| 441 | + | const issues = issuesOpened.count + issuesClosed.count + comments.count; | |
| 442 | + | if (issues > 0) { | |
| 443 | + | const text = counted([ | |
| 444 | + | [issuesOpened.count, "# opened"], | |
| 445 | + | [issuesClosed.count, "# closed"], | |
| 446 | + | [comments.count, comments.count === 1 ? "# comment" : "# comments"], | |
| 447 | + | ]); | |
| 448 | + | lines.push({ key: "issues", text, count: issues, to: placeFor(slug, repos, [...issuesOpened.repoIds, ...issuesClosed.repoIds, ...comments.repoIds], "issues") }); | |
| 398 | 449 | } | |
| 399 | − | const comments = sum((repo) => repo.comments); | |
| 400 | − | if (comments.count > 0) { | |
| 401 | − | lines.push({ key: "comments", text: `left ${plural(comments.count, "comment")}`, to: placeFor(slug, repos, comments.repoIds, "issues") }); | |
| 402 | − | credit(comments.actors); | |
| 403 | − | } | |
| 404 | − | const succeeded = sum((repo) => repo.deployments.succeeded); | |
| 405 | − | const failed = sum((repo) => repo.deployments.failed); | |
| 406 | − | if (succeeded.count + failed.count > 0) { | |
| 407 | − | const text = succeeded.count > 0 ? `${plural(succeeded.count, "deploy")} went out${failed.count > 0 ? `, ${failed.count.toLocaleString("en-US")} failed` : ""}` : `${plural(failed.count, "deploy")} failed`; | |
| 408 | − | lines.push({ key: "deploys", text, to: placeFor(slug, repos, [...succeeded.repoIds, ...failed.repoIds], "deployments") }); | |
| 409 | − | credit(succeeded.actors); | |
| 410 | − | credit(failed.actors); | |
| 411 | − | } | |
| 412 | − | const releases = sum((repo) => tallyOf(repo.releases.map((release) => release.actor))); | |
| 413 | − | if (releases.count > 0) { | |
| 414 | − | lines.push({ key: "releases", text: `published ${plural(releases.count, "release")}`, to: placeFor(slug, repos, releases.repoIds, "releases") }); | |
| 415 | − | credit(releases.actors); | |
| 416 | − | } | |
| 417 | − | const packages = sum((repo) => tallyOf(repo.packages.map((pkg) => pkg.actor))); | |
| 418 | − | if (packages.count > 0) { | |
| 419 | − | lines.push({ key: "packages", text: `published ${plural(packages.count, "package version")}`, to: `/${slug}/-/packages` }); | |
| 420 | − | credit(packages.actors); | |
| 450 | + | ||
| 451 | + | const succeeded = sum("shipped", (repo) => repo.deployments.succeeded); | |
| 452 | + | const failed = sum("shipped", (repo) => repo.deployments.failed); | |
| 453 | + | const releases = sum("shipped", (repo) => tallyOf(repo.releases.map((release) => release.actor))); | |
| 454 | + | const packages = sum("shipped", (repo) => tallyOf(repo.packages.map((pkg) => pkg.actor))); | |
| 455 | + | const shipped = succeeded.count + failed.count + releases.count + packages.count; | |
| 456 | + | if (shipped > 0) { | |
| 457 | + | const text = counted([ | |
| 458 | + | [succeeded.count, succeeded.count === 1 ? "# deploy" : "# deploys"], | |
| 459 | + | [failed.count, "# failed"], | |
| 460 | + | [releases.count, releases.count === 1 ? "# release" : "# releases"], | |
| 461 | + | [packages.count, packages.count === 1 ? "# package version" : "# package versions"], | |
| 462 | + | ]); | |
| 463 | + | const deployed = [...succeeded.repoIds, ...failed.repoIds]; | |
| 464 | + | const to = | |
| 465 | + | deployed.length > 0 ? placeFor(slug, repos, deployed, "deployments") : releases.count > 0 ? placeFor(slug, repos, releases.repoIds, "releases") : `/${slug}/-/packages`; | |
| 466 | + | lines.push({ key: "shipped", text, count: shipped, to }); | |
| 421 | 467 | } | |
| 422 | 468 | ||
| 423 | 469 | // Artifacts: made, and whose content changed. | |
| 424 | 470 | if (activity.folios) { | |
| 425 | − | const made = part(activity.folios.created, agentKeys); | |
| 426 | − | const edited = new Set<string>(); | |
| 427 | − | const editors = new Map<string, number>(); | |
| 471 | + | const made = credit("docs", activity.folios.created); | |
| 472 | + | const edits: ActorCount[] = []; | |
| 473 | + | let edited = 0; | |
| 428 | 474 | let allDocs = true; | |
| 429 | 475 | for (const folio of activity.folios.edited) { | |
| 430 | − | const mine = folio.authors.filter((author) => actorKind(author, agentKeys) === "person"); | |
| 431 | − | if (mine.length === 0) continue; | |
| 432 | − | edited.add(folio.folio_id); | |
| 476 | + | if (folio.authors.length === 0) continue; | |
| 477 | + | edited += 1; | |
| 433 | 478 | if (folio.kind !== "doc") allDocs = false; | |
| 434 | − | for (const author of mine) editors.set(author, (editors.get(author) ?? 0) + 1); | |
| 479 | + | for (const author of folio.authors) edits.push({ actor: author, count: 1 }); | |
| 435 | 480 | } | |
| 436 | − | const what = (n: number) => plural(n, allDocs ? "doc" : "artifact"); | |
| 437 | − | if (made.count + edited.size > 0) { | |
| 481 | + | credit("docs", edits); | |
| 482 | + | const what = (count: number) => plural(count, allDocs ? "doc" : "artifact"); | |
| 483 | + | if (made + edited > 0) { | |
| 438 | 484 | const parts: string[] = []; | |
| 439 | − | if (made.count > 0) parts.push(`created ${what(made.count)}`); | |
| 440 | − | if (edited.size > 0) parts.push(`edited ${what(edited.size)}`); | |
| 441 | − | lines.push({ key: "docs", text: parts.join(", "), to: `/${slug}/-/artifacts` }); | |
| 442 | − | credit(made.actors); | |
| 443 | − | credit(editors); | |
| 485 | + | if (made > 0) parts.push(`${what(made)} created`); | |
| 486 | + | if (edited > 0) parts.push(`${what(edited)} edited`); | |
| 487 | + | lines.push({ key: "docs", text: parts.join(", "), count: made + edits.length, to: `/${slug}/-/artifacts` }); | |
| 444 | 488 | } | |
| 445 | 489 | } | |
| 446 | 490 | } | |
| 447 | 491 | ||
| 448 | − | // Chat: what people said. | |
| 492 | + | // Chat: what was said. | |
| 449 | 493 | if (chat) { | |
| 450 | − | const said = part( | |
| 494 | + | const said = credit( | |
| 495 | + | "chat", | |
| 451 | 496 | chat.authors.map((author) => ({ actor: author.key, count: author.messages })), | |
| 452 | − | agentKeys, | |
| 453 | 497 | ); | |
| 454 | − | if (said.count > 0) { | |
| 455 | − | lines.push({ key: "chat", text: `sent ${plural(said.count, "message")} in ${plural(chat.channels, "conversation")}`, to: `/${slug}/-/chat` }); | |
| 456 | − | credit(said.actors); | |
| 498 | + | if (said > 0) { | |
| 499 | + | lines.push({ key: "chat", text: `${plural(said, "message")} in ${plural(chat.channels, "conversation")}`, count: said, to: `/${slug}/-/chat` }); | |
| 457 | 500 | } | |
| 458 | 501 | } | |
| 459 | 502 | ||
| 460 | − | const actors: DigestActor[] = [...did] | |
| 461 | − | .map(([key, count]) => ({ key, name: names[key] ?? "someone", count })) | |
| 462 | − | .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)); | |
| 503 | + | const actors = [...rows.values()].filter((actor) => actor.count > 0).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)); | |
| 463 | 504 | return { lines, actors }; | |
| 464 | 505 | } | |
| 465 | 506 | ||
| ⋯ | |||
| 489 | 530 | return { | |
| 490 | 531 | people: column, | |
| 491 | 532 | summary: { | |
| 492 | − | people: column.actors.length, | |
| 533 | + | people: column.actors.filter((actor) => actor.kind === "person").length, | |
| 493 | 534 | commits, | |
| 494 | 535 | merged, | |
| 495 | 536 | said, | |
Comments
Sign in to comment.