Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

Home leads with Ask, then your move beside what's live, then the week day by day (Closes #167)

Home was laid out like another product's daily brief. Its headline could also say "nothing needs a closer look" right above "3 things need you". Home now reads from what you can do now to what already happened: - What are we building today?: the box that asks g1t, at the top of the page. - Your move: everything waiting on you, with Start here first. Beside it, Live: the agents at work (each session with who runs it, how long it has gone, its subagents and its cost so far at list price), then workflow runs and builds. - This week: - The week in a sentence, then four figures, renamed: Landed by agents, Cost per agent change, Gone quiet, Sent back. - The last 14 days as columns, agents' and people's changes stacked, last week faded. - Worth a look: the findings, each pinned over the day it happened when it has one (a milestone closing, someone's first merge in a project). - It says nothing about what needs you, because Your move says that. - Since you were last here, beside Spent and Settled for you. The page is wider (90rem, side column 24rem). Findings name the change itself: - "tax#4821 holds up three work items, and no one has reviewed it yet." - "The Checkout replatform took more than half of the agent spend this quarter." The Home guide and the links to it follow. Checked: - apps/web tsc -b; npm test (1,096). - npx prettier --check . - Screenshots of the new parts with sample data, dark and light at 1440 px and a phone at 390 px.

G1t-Session:
ses_01m57b7r6jew3aqgyfmhw6yah6
syntaqxcommitted Parent5a992f0Browse files
10 files+650−4290/10 viewed
+78−71
11 ---
22 title: Home
3−description: Your workspace's front page. The past week in four figures, the daily signal (what your work says today, with the evidence), where you're needed now, what happened since you were last here in plain English, what is running, and the one item to start with. How every number is worked out.
3+description: Your workspace's front page. A box that asks g1t, your move (everything waiting on you, and the one item to start with), the agents, runs and builds going now, this week's work day by day with what is worth a look, and what happened since you were last here in plain English. How every number is worked out.
44 ---
55
66 **Home** is the first page of your workspace, at `/<workspace>/-/home`.
7−Across the top is [the past week](#the-past-week) in four figures, each
8−against the week before. Under it, the main column is read top to
9−bottom:
7+It reads top to bottom, from what you can do now to what already happened:
108
11−1. **[The daily signal](#the-daily-signal)**: what the workspace's work
12− says today. Its headline is the finding that matters most, and the
13− others follow, each a sentence with its evidence and where to see it.
14− Under them is a box that [asks g1t](#ask-from-home) about any of it.
15−2. **Where you're needed**: everything waiting on you, however long ago
9+1. **What are we building today?**: a box that [asks g1t](#ask-from-home)
10+ anything about your work, with questions to start with.
11+2. **[Your move](#your-move)**: everything waiting on you, however long ago
1612 it started, with the action beside each item. Its first row is
17− [Start here](#start-here), the one item to do first, and why.
18−3. **Since you were last here**: one card for the span. It shows what people
13+ [Start here](#start-here), the one item to do first, and why. Beside it,
14+ **[Live](#live)**: the agents at work, workflow runs and builds going
15+ now, each agent session with what it has cost so far.
16+3. **[This week](#this-week)**: the week in a sentence and four figures,
17+ the last two weeks day by day (what agents and people landed), and what
18+ is [worth a look](#worth-a-look), each finding with its evidence and
19+ pinned to the day it happened where it happened on one.
20+4. **Since you were last here**: one card for the span. It shows what people
1921 did, what changed in plain English when you ask for it, and what landed,
20− each change with whether it is live yet.
21−
22−The side column stays in view as you scroll:
23−
24−- **[Running now](#running-now)**: workflow runs and builds still going.
25−- **[Spent](#spent)**: what the span cost, and the month so far.
26−- **[Settled for you](#settled-for-you)**: what finished by itself.
22+ each change with whether it is live yet. Beside it, kept in view as you
23+ scroll: **[Spent](#spent)**, what the span cost and the month so far,
24+ and **[Settled for you](#settled-for-you)**, what finished by itself.
2725
28−On a phone it is one column: the past week, the daily signal, Where
29−you're needed, Running now, the span, then Spent and Settled for you.
26+On a phone it is one column, in the same order.
3027
3128 For a workspace's owners, Home also opens
3229 [Getting started](/guides/workspaces/#getting-started), the setup guide,
3936 so, and the rest of the page still shows. Only what you ask g1t is written
4037 by a model: [What changed](#what-changed) and the answers in Ask.
4138
42−## The past week
39+## This week
4340
44−Four figures for the last seven UTC days. Each shows how it moved since the
45−seven days before (green when that is good news, red when it is not) and a
46−line over the last ten weeks. Point at the line, or focus it and use the
47−arrow keys, to read any week. Each figure opens the page behind it.
41+A sentence for what landed in the last seven UTC days and how much of it
42+was agents', then four figures. Each shows how it moved since the seven
43+days before (green when that is good news, red when it is not); point at
44+one for what it counts, and it opens the page behind it.
4845
49−| Figure | What it counts |
50−| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51−| **Agent-authored changes** | Pull requests that merged in the week and were an [agent's](#whose-work-is-an-agents), and the commits agent sessions pushed outside any pull request (to the default branch, most of all), each once. A workspace that pushes straight to main lands most of its work the second way. |
52−| **Spend per agent change** | The week's agent [usage](/guides/usage-and-billing/#the-usage-page) at price, plus what the [sessions](/guides/sessions/) started in it cost (estimated at list price from their token usage), divided by the agent changes that landed in it: an average, not what any one cost. A dash when usage can't be read or no agent change landed. |
53−| **Stalled changes** | Pull requests open at the end of each week (now, for this one), opened three days or more before, with nothing in the three days before: no push, comment, review, timeline event, check or session. Drafts count. |
54−| **Agent rework rate** | Of the agent pull requests that merged in the week (direct commits get no review, so they're not counted here), the share a person asked for changes on before they merged. |
46+Under them are the last fourteen UTC days as columns: what agents landed
47+(their merged pull requests and the commits their sessions pushed
48+directly) in the accent, what people merged above it, last week's days
49+faded. Point at a day, or focus it, to read it in full: what landed, by
50+whom, and the sessions started. A finding that happened on one day, such
51+as a milestone that closed, is pinned over it with the number it has in
52+[Worth a look](#worth-a-look).
53+
54+| Figure | What it counts |
55+| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
56+| **Landed by agents** | Pull requests that merged in the week and were an [agent's](#whose-work-is-an-agents), and the commits agent sessions pushed outside any pull request (to the default branch, most of all), each once. A workspace that pushes straight to main lands most of its work the second way. |
57+| **Cost per agent change** | The week's agent [usage](/guides/usage-and-billing/#the-usage-page) at price, plus what the [sessions](/guides/sessions/) started in it cost (estimated at list price from their token usage), divided by the agent changes that landed in it: an average, not what any one cost. A dash when usage can't be read or no agent change landed. |
58+| **Gone quiet** | Pull requests open at the end of each week (now, for this one), opened three days or more before, with nothing in the three days before: no push, comment, review, timeline event, check or session. Drafts count. |
59+| **Sent back** | Of the agent pull requests that merged in the week (direct commits get no review, so they're not counted here), the share a person asked for changes on before they merged. |
5560
5661 ### Whose work is an agent's
5762
6166 for it, else its author, so an agent's work counts toward the person it
6267 was for.
6368
64−## The daily signal
69+### Worth a look
6570
66−First the day, then one sentence for the finding that matters most today,
67−then the others. Each finding names its evidence in a line of figures,
68−links to where you can see it, and has **Ask g1t**, which asks
69−[Ask](/guides/ask/) to look into it. A finding with a trend has its line
70−beside it. When nothing stands out, the headline says what the week did.
71+Under the days, what the work says is worth a look, most important first.
72+Each finding is a sentence with its evidence after it, a link to where you
73+can see it, and **Ask g1t**, which asks [Ask](/guides/ask/) to look into
74+it. A finding with a trend has its line beside it. When nothing stands
75+out against the workspace's last weeks, it says so. None of it is about
76+what needs you: that is [Your move](#your-move).
7177
72−| Finding | When it shows |
73−| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
74−| **Work items waiting on one unreviewed change** | An open pull request no one has reviewed is for a work item that other open work items are blocked by. The one holding up the most leads. |
75−| **Agent drafts waiting for review** | This week's first reviews waited at least four times as long as agents took to open the pull requests (from their session starting), and at least four hours. Without that gap: first reviews took half as long again as the weeks before. |
76−| **A milestone shipped** | A milestone closed in the last seven days, against its due date, with its weeks in build, its merged pull requests and its projects. |
77−| **Where agent spend is going** | More than a third of the quarter's agent spend by pull request went into one milestone (else one work item, else one project). Spend is g1t's usage on each pull request and its share of its sessions' cost. |
78−| **Stalled changes rising** | At least three more stalled changes than last week, and a quarter more. |
79−| **Agent rework moving** | People asked for changes on agents' merged work at least 8 points more, or less, often than over the four weeks before (with five agent merges or more). |
80−| **A practice catching on** | Two or more people started using a harness (Claude Code, Codex, Cursor and so on) this month, at least two weeks after the first person did. |
81−| **Who works where** | In a workspace where three people or more merged lately, someone started merging to a project one person has carried (three quarters of its last 90 days' merges); or merged to a project for the first time this week after work elsewhere. |
78+| Finding | When it shows |
79+| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
80+| **One unreviewed change holding up work** | An open pull request no one has reviewed is for a work item that other open work items are blocked by. The one holding up the most leads. |
81+| **Agent drafts waiting for review** | This week's first reviews waited at least four times as long as agents took to open the pull requests (from their session starting), and at least four hours. Without that gap: first reviews took half as long again as the weeks before. |
82+| **A milestone closed** | A milestone closed in the last seven days, against its due date, with its weeks in build, its merged pull requests and its projects. |
83+| **Where agent spend is going** | More than a third of the quarter's agent spend by pull request went into one milestone (else one work item, else one project). Spend is g1t's usage on each pull request and its share of its sessions' cost. |
84+| **More changes gone quiet** | At least three more stalled changes than last week, and a quarter more. |
85+| **Agent rework moving** | People asked for changes on agents' merged work at least 8 points more, or less, often than over the four weeks before (with five agent merges or more). |
86+| **A practice catching on** | Two or more people started using a harness (Claude Code, Codex, Cursor and so on) this month, at least two weeks after the first person did. |
87+| **Who works where** | In a workspace where three people or more merged lately, someone started merging to a project one person has carried (three quarters of its last 90 days' merges); or merged to a project for the first time this week after work elsewhere. |
8288
8389 Findings are ordered by how much they hold up: blocked work first, then
84−waits, then what shipped and where spend went. Up to five show, and the
90+waits, then what closed and where spend went. Up to four show, and the
8591 rest are one click away. What the work says is read again at most every
8692 five minutes for each person, so a figure can be up to five minutes
8793 behind. The same reads, at more length, are on the
8995
9096 ## Ask from Home
9197
92−The box under the daily signal asks g1t anything about your work. Type a question and
98+The box at the top of Home asks g1t anything about your work. Type a question and
9399 press **Enter** (**Shift+Enter** starts a new line): the top bar's
94100 [Ask](/guides/ask/) panel opens and sends it, and the answer streams in
95101 there. Under the box are questions to start with:
109115 still answering an earlier question, the panel opens with your question
110116 in its box rather than sending it, so nothing you typed is lost.
111117
112−## Where you're needed
118+## Your move
113119
114120 This list has no time window: something that started waiting a week ago
115121 is here until you deal with it. Each row says what it is, what is at
145151 6. Mentions and direct messages.
146152
147153 The list shows the first eight rows; **Show more** opens the rest. The
148−sentence under the page's heading starts with how many things need you.
154+line under its heading says how many things need you.
149155
150156 ## Start here
151157
152−**Start here** is the first row of Where you're needed, set apart at the
158+**Start here** is the first row of Your move, set apart at the
153159 top: the most pressing tier, and within it the row that has waited
154160 longest. g1t picks it with that rule, so the same rows always give the
155161 same pick. It says why, such as "Everything it needs is met; it lands when
192198 - With no visit on record, Home shows the last 24 hours and says so. After
193199 more than 14 days away, it shows the last 14 days.
194200
195−The sentence under the heading sums it up, for example:
201+The sentence under the card's heading sums it up, for example:
196202
197−> 8 things need you. Since Tuesday evening, 3 people pushed 41 commits and
198−> merged 5 changes, and 12 changes landed.
203+> Since Tuesday evening, 3 people pushed 41 commits and merged 5
204+> changes, and 12 changes landed.
199205
200206 It says what people did first (the commits they pushed and the changes
201207 they merged; if neither, the messages they sent), then everything that
327333 requests, pushes and releases, deployments) didn't answer, the card says
328334 so under the list.
329335
330−## Running now
336+## Live
331337
332−At the top of the side column, what is going right now, longest-going
333−first, each with its state and how long it has gone:
338+Beside Your move, what is going right now. First the agents at work: each
339+live [session](/guides/sessions/) of a harness (Claude Code, Codex and so
340+on) or a hosted agent, with what it is on, who runs it, how long it has
341+gone, its subagents, and what it has cost so far, estimated at list price
342+from its token usage. Then workflow runs and builds, longest-going first,
343+each with its state and how long it has gone:
334344
335345 | Kind | What it is |
336346 | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
339349
340350 ## Spent
341351
342−In the side column: what the workspace spent over the span, from its
352+Beside the span: what the workspace spent over it, from its
343353 [statement](/guides/usage-and-billing/), with each line at its price: what
344354 was charged plus what your plan's included usage, a trial, a pool or a
345355 discount paid of it. The lines are the statement's kinds of charge, such
356366
357367 ## Settled for you
358368
359−At the bottom of the side column: your notifications in this workspace
369+Under Spent: your notifications in this workspace
360370 that [settled themselves](/guides/notifications/#threads-that-settle-themselves)
361371 in the span, newest first, each with why, such as **CI passed in run 330**
362372 or **Merged by @ana**: a later run passed, the pull request merged, the
368378 Home keeps up with the workspace without a reload. What lands, starts
369379 running, finishes or comes to need you while you are on the page joins its
370380 list, sliding in at its place and lit for a moment; counts and the
371−sentence under the heading update with it. Runs that finish leave Running
372−now.
381+lines update with it. Sessions that end and runs that finish leave Live.
373382
374383 | Home updates when | From |
375384 | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
394403 A member whose membership does not include Code sees Home without anything
395404 from Code:
396405
397−- The past week and the daily signal are not shown. The page opens with
398− **What are we building today?** and the box that asks g1t.
406+- This week is not shown.
399407 - People shows what was said and the artifacts edited; nothing from
400408 repositories.
401−- Where you're needed has no rows from Code, and no notifications about a
409+- Your move has no rows from Code, and no notifications about a
402410 repository.
403−- Landed and Running now are not shown.
411+- Landed is not shown, and Live has no sessions, runs or builds.
404412 - Spent is the same for every member.
405413
406414 ## When something doesn't answer
408416 Each section reads its own part of g1t and shows as soon as it has it.
409417 When one does not answer:
410418
411−- Where you're needed and Running now name the parts that didn't answer,
419+- Your move and Live name the parts that didn't answer,
412420 so an empty list never reads as nothing waiting.
413421 - People counts what could be read, and names the part that is missing
414422 (Code or Chat). If nothing could be read, the card says so, and the
415− sentence under the heading says what people did couldn't be read.
423+ sentence under the card's heading says what people did couldn't be read.
416424 - Landed and Spent each say their part didn't answer. When
417425 deployments or sessions can't be read, Landed shows its rows without
418426 their live labels or agent markers.
419427 - Settled for you says when your notifications can't be read.
420−- The daily signal says when the work couldn't be read, and the past week
421− is left out. When some projects didn't answer, it says its findings
422− leave them out.
428+- This week says when the work couldn't be read. When some projects
429+ didn't answer, it says the week leaves them out.
423430 - What changed says when Ask can't be reached or isn't set up, or when
424431 the question could not be sent.
425432 - If your last visit can't be read, the span is the last 24 hours and the
+2−2
9999 A thread that leaves you nothing to do moves to Done by itself, read, the
100100 moment what it told you of is over: a later run passes, the pull request
101101 merges, the review is no longer asked of you. It leaves your unread count,
102−the bell, [Home](/guides/home/#where-youre-needed)'s **Where you're
103−needed** and `g1t status`, and
102+the bell, [Home](/guides/home/#your-move)'s **Your
103+move** and `g1t status`, and
104104 stays under **Done** with its history. New activity brings it back, as with
105105 any thread.
106106
+2−2
675675
676676 The workspace's **Overview**, `g1t.sh/<workspace>/-/workspace`, opens with
677677 what its work says, for members who can see Code. It is worked out the
678−same way as [Home's daily signal](/guides/home/#the-daily-signal), and
678+same way as [Home's week](/guides/home/#this-week), and
679679 each person sees only the projects they can read.
680680
681−1. **[The past week](/guides/home/#the-past-week)**: the same four
681+1. **[The past week](/guides/home/#this-week)**: the same four
682682 figures as Home.
683683 2. **Spend, threaded to the work**: the quarter's work in four columns:
684684 each milestone, its work items, each work item's change set in each
+9−18
3434 }
3535
3636 /**
37− * Home's box that asks g1t, with a few things to ask first: under the
38− * daily signal, `compact`, it says to ask about any of it; on its own, it
39− * leads the page with what are we building today.
37+ * Home's box that asks g1t, with a few things to ask first. It leads the
38+ * page, above everything it can be asked about, with what are we building
39+ * today.
4040 */
41−export function HomeAsk({ span, compact = false }: { span: AskSpan; compact?: boolean }) {
41+export function HomeAsk({ span }: { span: AskSpan }) {
4242 const [draft, setDraft] = useState("");
4343 const send = () => {
4444 if (!draft.trim()) return;
5656 }
5757 };
5858 return (
59− <section
60− aria-labelledby="home-ask"
61− className={compact ? "flex w-full flex-col gap-3" : "mx-auto flex w-full max-w-3xl flex-col items-center gap-5 pt-4 pb-2 md:pt-8"}
62− >
63− {compact ? (
64− <h2 id="home-ask" className="text-sm font-semibold text-fg">
65− Ask about any of this
66− </h2>
67− ) : (
68− <h1 id="home-ask" className="text-center text-[1.75rem] leading-tight font-semibold tracking-tight md:text-[2rem]">
69− What are we building today?
70− </h1>
71− )}
59+ <section aria-labelledby="home-ask" className="mx-auto flex w-full max-w-5xl flex-col items-center gap-5 pt-4 pb-3 md:pt-8 md:pb-5">
60+ <h1 id="home-ask" className="text-center text-[1.75rem] leading-tight font-semibold tracking-tight md:text-[2rem]">
61+ What are we building today?
62+ </h1>
7263 <form onSubmit={submit} className="w-full">
7364 <div className="flex items-end gap-2 rounded-2xl border border-line bg-surface p-2 pl-4 shadow-sm transition-shadow focus-within:border-accent-dim focus-within:ring-2 focus-within:ring-accent/25">
7465 <MarkGlyph className="mb-2.5 h-4 shrink-0 max-sm:hidden" />
9283 </Hint>
9384 </div>
9485 </form>
95− <Suggestions className={compact ? "" : "justify-center"}>
86+ <Suggestions className="justify-center">
9687 <Suggestion
9788 suggestion={`Summarize what changed ${spanLabel(span)}`}
9889 onPick={() => window.dispatchEvent(new CustomEvent(SUMMARIZE_EVENT))}
+278−0
1+/**
2+ * Home's week (lib/signals.ts): the week in a sentence and four figures,
3+ * the last two weeks day by day (what agents and people landed), and what
4+ * is worth a look, each pinned to the day it happened where it happened
5+ * on one. The page leads with what needs you; this is the work's own
6+ * account, so it never says whether anything needs anyone.
7+ */
8+import { ChevronRight, MessageSquare, TriangleAlert } from "lucide-react";
9+import { useState } from "react";
10+import { Link } from "react-router";
11+
12+import { askG1t } from "../lib/ask";
13+import { cn } from "../lib/cn";
14+import type { DailySignal, DayBar, PulseTile, Signal } from "../lib/signals";
15+import { TrendLine } from "./signals";
16+import { Hint } from "./ui/hint";
17+import { Skeleton } from "./ui/skeleton";
18+
19+const DELTA_TONE: Record<"good" | "bad" | "flat", string> = { good: "text-success", bad: "text-danger", flat: "text-faint" };
20+
21+/** One figure: what it counts, this week's value, and the change on last week. */
22+function Figure({ tile }: { tile: PulseTile }) {
23+ return (
24+ <Hint label={tile.hint}>
25+ <Link to={tile.to} className="group min-w-0 rounded-lg px-3 py-2 transition-colors hover:bg-raised">
26+ <p className="truncate text-xs text-muted group-hover:text-fg">{tile.label}</p>
27+ <p className="mt-0.5 flex items-baseline gap-2">
28+ <span className="text-xl font-semibold tracking-tight text-fg tabular-nums">{tile.value}</span>
29+ {tile.delta && (
30+ <span className={cn("text-xs tabular-nums", DELTA_TONE[tile.delta.tone])}>
31+ {tile.delta.text}
32+ <span className="text-faint"> on last week</span>
33+ </span>
34+ )}
35+ </p>
36+ </Link>
37+ </Hint>
38+ );
39+}
40+
41+function dayName(day: string, style: "short" | "long"): string {
42+ const date = new Date(`${day}T00:00:00Z`);
43+ return style === "short"
44+ ? date.toLocaleDateString("en-US", { weekday: "narrow", timeZone: "UTC" })
45+ : date.toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric", timeZone: "UTC" });
46+}
47+
48+function dayWords(bar: DayBar): string {
49+ const landed = bar.agent + bar.people;
50+ if (landed === 0 && bar.sessions === 0) return `${dayName(bar.day, "long")}: nothing landed`;
51+ const parts = [`${landed} landed`, `${bar.agent} by agents`, `${bar.people} by people`];
52+ if (bar.sessions > 0) parts.push(`${bar.sessions} ${bar.sessions === 1 ? "session" : "sessions"} started`);
53+ return `${dayName(bar.day, "long")}: ${parts.join(", ")}`;
54+}
55+
56+/**
57+ * Two weeks of days as columns: agents' changes in the accent, people's
58+ * above them, last week's faded. A finding that happened on a day is
59+ * pinned over it with its number in the list below.
60+ */
61+function Days({ days, pins }: { days: DayBar[]; pins: Map<string, number[]> }) {
62+ const [active, setActive] = useState<number | null>(null);
63+ const top = Math.max(1, ...days.map((bar) => bar.agent + bar.people));
64+ const thisWeek = days.length - 7;
65+ const shown = active ?? days.length - 1;
66+ return (
67+ <figure className="px-5">
68+ <div
69+ className="grid h-36 items-end gap-1.5 sm:gap-2"
70+ style={{ gridTemplateColumns: `repeat(${days.length}, minmax(0, 1fr))` }}
71+ onPointerLeave={() => setActive(null)}
72+ >
73+ {days.map((bar, i) => {
74+ const landed = bar.agent + bar.people;
75+ const pinned = pins.get(bar.day);
76+ return (
77+ <button
78+ key={bar.day}
79+ type="button"
80+ aria-label={dayWords(bar)}
81+ onPointerEnter={() => setActive(i)}
82+ onFocus={() => setActive(i)}
83+ onBlur={() => setActive(null)}
84+ className={cn(
85+ "group relative flex h-full flex-col items-stretch justify-end rounded-md outline-none focus-visible:ring-2 focus-visible:ring-accent/40",
86+ i < thisWeek && "opacity-45",
87+ active === i && "opacity-100",
88+ )}
89+ >
90+ {pinned && (
91+ <span className="absolute inset-x-0 -top-1 flex justify-center gap-0.5">
92+ {pinned.map((n) => (
93+ <span key={n} className="flex size-4 items-center justify-center rounded-full bg-warn text-[0.625rem] font-semibold text-bg">
94+ {n}
95+ </span>
96+ ))}
97+ </span>
98+ )}
99+ <span
100+ className="mx-auto flex w-full max-w-11 flex-col justify-end overflow-hidden rounded-[5px]"
101+ style={{ height: `${Math.max(landed > 0 ? 6 : 2, (landed / top) * 80)}%` }}
102+ >
103+ {landed === 0 ? (
104+ <span className="h-full bg-line" />
105+ ) : (
106+ <>
107+ <span className="bg-line-strong transition-colors group-hover:bg-fg-soft/50" style={{ flexGrow: bar.people }} />
108+ <span className="bg-accent transition-colors group-hover:bg-accent-hover" style={{ flexGrow: bar.agent }} />
109+ </>
110+ )}
111+ </span>
112+ </button>
113+ );
114+ })}
115+ </div>
116+ <div className="mt-1.5 grid gap-1.5 sm:gap-2" style={{ gridTemplateColumns: `repeat(${days.length}, minmax(0, 1fr))` }} aria-hidden>
117+ {days.map((bar, i) => (
118+ <span key={bar.day} className={cn("text-center font-mono text-[0.625rem] text-faint", i === days.length - 1 && "text-fg-soft")}>
119+ {dayName(bar.day, "short")}
120+ </span>
121+ ))}
122+ </div>
123+ <figcaption className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted">
124+ <span className="text-fg-soft" aria-live="polite">
125+ {days[shown] ? dayWords(days[shown]) : ""}
126+ </span>
127+ <span className="ml-auto flex items-center gap-3">
128+ <span className="flex items-center gap-1.5">
129+ <span className="size-2 rounded-sm bg-accent" />
130+ Agents
131+ </span>
132+ <span className="flex items-center gap-1.5">
133+ <span className="size-2 rounded-sm bg-line-strong" />
134+ People
135+ </span>
136+ <span className="text-faint">last week faded</span>
137+ </span>
138+ </figcaption>
139+ </figure>
140+ );
141+}
142+
143+/** A finding: what the work says, the evidence after it, where to see it, and Ask. */
144+function Finding({ signal, n }: { signal: Signal; n: number | null }) {
145+ return (
146+ <li className={cn("grid gap-3 border-t border-line px-5 py-4", signal.trend && "md:grid-cols-[minmax(0,1fr)_9rem] md:gap-6")}>
147+ <div className="flex min-w-0 gap-3">
148+ <span
149+ className={cn(
150+ "mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full text-[0.6875rem] font-semibold",
151+ n != null ? "bg-warn text-bg" : "border border-line-strong text-faint",
152+ )}
153+ aria-hidden
154+ >
155+ {n ?? "·"}
156+ </span>
157+ <div className="min-w-0 space-y-1">
158+ <p className="text-sm leading-snug font-medium text-fg">{signal.headline}</p>
159+ <p className="text-xs leading-relaxed text-muted">{signal.facts.join(" · ")}</p>
160+ <div className="flex flex-wrap items-center gap-x-4 gap-y-1 pt-0.5">
161+ {signal.link && (
162+ <Link to={signal.link.to} className="inline-flex items-center gap-0.5 text-xs font-medium text-accent hover:underline">
163+ {signal.link.label}
164+ <ChevronRight size={12} />
165+ </Link>
166+ )}
167+ <button type="button" onClick={() => askG1t(signal.ask)} className="inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
168+ <MessageSquare size={11} />
169+ Ask g1t
170+ </button>
171+ </div>
172+ </div>
173+ </div>
174+ {signal.trend && (
175+ <div className="hidden md:block">
176+ <TrendLine
177+ values={signal.trend.values}
178+ points={signal.trend.points}
179+ label={`${signal.trend.caption}, ${signal.trend.range}`}
180+ width={144}
181+ height={32}
182+ />
183+ <p className="mt-1 font-mono text-[0.625rem] text-faint">{signal.trend.range}</p>
184+ </div>
185+ )}
186+ </li>
187+ );
188+}
189+
190+/** Findings shown before "N more". */
191+const SHOWN = 4;
192+
193+/** Home's week: its figures, its days with findings pinned, and what is worth a look. */
194+export function HomeWeek({ view }: { view: DailySignal | null }) {
195+ const [more, setMore] = useState(false);
196+ if (view == null) {
197+ return (
198+ <section aria-labelledby="week" className="rounded-2xl border border-line bg-surface px-5 py-4">
199+ <h2 id="week" className="text-base font-semibold tracking-tight text-fg">
200+ This week
201+ </h2>
202+ <p className="mt-2 flex items-center gap-1.5 text-sm text-muted">
203+ <TriangleAlert size={13} className="shrink-0 text-warn" />
204+ The week&apos;s work couldn&apos;t be read just now.
205+ </p>
206+ </section>
207+ );
208+ }
209+ // A number for each finding pinned to a day in the chart, in the list's order.
210+ const days = new Set(view.days.map((bar) => bar.day));
211+ const numbers = new Map<string, number>();
212+ const pins = new Map<string, number[]>();
213+ for (const signal of view.findings) {
214+ if (!signal.day || !days.has(signal.day)) continue;
215+ const n = numbers.size + 1;
216+ numbers.set(signal.key, n);
217+ pins.set(signal.day, [...(pins.get(signal.day) ?? []), n]);
218+ }
219+ const shown = more ? view.findings : view.findings.slice(0, SHOWN);
220+ return (
221+ <section aria-labelledby="week" className="rounded-2xl border border-line bg-surface">
222+ <header className="flex flex-wrap items-baseline gap-x-3 gap-y-1 px-5 pt-4">
223+ <h2 id="week" className="text-base font-semibold tracking-tight text-fg">
224+ This week
225+ </h2>
226+ <p className="text-sm text-muted">{view.summary}</p>
227+ </header>
228+ {view.tiles.length > 0 && (
229+ <div className="mt-2 grid grid-cols-2 gap-1 px-2 lg:grid-cols-4">
230+ {view.tiles.map((tile) => (
231+ <Figure key={tile.key} tile={tile} />
232+ ))}
233+ </div>
234+ )}
235+ <div className="mt-4">
236+ <Days days={view.days} pins={pins} />
237+ </div>
238+ <div className="mt-5">
239+ <h3 className="px-5 pb-2 text-xs font-medium text-muted">Worth a look</h3>
240+ {view.findings.length === 0 ? (
241+ <p className="border-t border-line px-5 py-4 text-sm text-muted">Nothing in the work stands out against its last weeks.</p>
242+ ) : (
243+ <ul>
244+ {shown.map((signal) => (
245+ <Finding key={signal.key} signal={signal} n={numbers.get(signal.key) ?? null} />
246+ ))}
247+ </ul>
248+ )}
249+ {view.findings.length > SHOWN && (
250+ <button type="button" onClick={() => setMore(!more)} className="border-t border-line px-5 py-3 text-left text-xs text-muted hover:text-fg w-full">
251+ {more ? "Show fewer" : `${view.findings.length - SHOWN} more worth a look`}
252+ </button>
253+ )}
254+ </div>
255+ {!view.complete && (
256+ <p className="flex items-center gap-1.5 border-t border-line px-5 py-2.5 text-xs text-faint">
257+ <TriangleAlert size={12} className="shrink-0" />
258+ Some projects didn&apos;t answer, so the week leaves them out.
259+ </p>
260+ )}
261+ <div className="h-2" />
262+ </section>
263+ );
264+}
265+
266+export function HomeWeekSkeleton() {
267+ return (
268+ <section className="space-y-4 rounded-2xl border border-line bg-surface px-5 py-4" aria-busy="true">
269+ <Skeleton className="h-5 w-72" />
270+ <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
271+ {[0, 1, 2, 3].map((i) => (
272+ <Skeleton key={i} className="h-10" />
273+ ))}
274+ </div>
275+ <Skeleton className="h-36 w-full" />
276+ </section>
277+ );
278+}
+62−7
11 /**
2− * Home's parts (routes/workspace/home.tsx): Where you're needed, Start
3− * here, what happened in the span (what people did, what landed, builds,
4− * spend) and Running now. Each takes what
2+ * Home's parts (routes/workspace/home.tsx): Your move, Start here, what
3+ * happened in the span (what people did, what landed, builds, spend) and
4+ * Live (sessions, runs and builds going now). Each takes what
55 * lib/home.ts worked out and only draws it; a row that arrived while the
66 * page was open comes in (`animate-arrive`, lib/use-home-live.ts).
77 */
2828 import type { ReactNode } from "react";
2929 import { Link } from "react-router";
3030
31+import type { WorkspaceSession } from "@g1t/contracts";
32+
3133 import {
3234 type Attention,
3335 type AttentionKind,
6062 import { useArrived } from "../lib/use-home-live";
6163 import { cn } from "../lib/cn";
6264 import { money } from "../lib/money";
65+import { repoName, runsIn, sessionHref, workspaceSessionTitle } from "../lib/sessions";
6366 import { CommitText } from "./commit-message";
67+import { HarnessMark, StateDot } from "./sessions";
6468 import { Button } from "./ui/button";
6569 import { Hint } from "./ui/hint";
6670 import { ButtonLink } from "./ui";
842846 deploy: <Rocket size={15} className="text-info" />,
843847 };
844848
845−export function RunningList({ rows, missing, now }: { rows: RunningRow[]; missing: string[]; now: number }) {
849+export function RunningList({ rows, missing, now, sessions = 0 }: { rows: RunningRow[]; missing: string[]; now: number; sessions?: number }) {
846850 const arrived = useArrived(rows.map((row) => row.key));
847851 return (
848852 <>
849853 {rows.length === 0 ? (
850− <Quiet>
851− {missing.length > 0 ? "Nothing is running in what could be read." : "Nothing is running. Workflow runs and builds show here while they go."}
852− </Quiet>
854+ // With sessions going above it, an empty list of runs says nothing.
855+ sessions > 0 ? null : (
856+ <Quiet>
857+ {missing.length > 0
858+ ? "Nothing is running in what could be read."
859+ : "Nothing is running. Agents at work, workflow runs and builds show here while they go."}
860+ </Quiet>
861+ )
853862 ) : (
854863 <Folded
855864 items={rows}
878887 </>
879888 );
880889 }
890+
891+/**
892+ * The agents at work now, each with what it is on, who runs it, how long
893+ * it has gone and what it has cost so far (list price, from its token
894+ * usage). Only g1t sees these: they are its sessions.
895+ */
896+export function LiveSessions({ sessions, slug, now }: { sessions: WorkspaceSession[] | null; slug: string; now: number }) {
897+ const arrived = useArrived((sessions ?? []).map((session) => session.id));
898+ if (sessions == null) return <p className="px-5 pb-3 text-xs text-faint">Sessions couldn&apos;t be read just now.</p>;
899+ if (sessions.length === 0) return null;
900+ return (
901+ <Folded
902+ items={sessions}
903+ render={(session) => {
904+ const to = sessionHref(slug, session);
905+ const cost = session.session?.costUsd;
906+ const title = workspaceSessionTitle(session);
907+ return (
908+ <li key={session.id} className={cn(rowClass, "items-start px-4", arrived.has(session.id) && ARRIVED)}>
909+ <span className="mt-1">
910+ <StateDot state={session.state} />
911+ </span>
912+ <div className="min-w-0 grow">
913+ {to ? (
914+ <Link to={to} className="line-clamp-2 text-sm text-fg hover:underline">
915+ {title}
916+ </Link>
917+ ) : (
918+ <p className="line-clamp-2 text-sm text-fg">{title}</p>
919+ )}
920+ <p className="flex min-w-0 items-center gap-1.5 truncate text-xs text-muted">
921+ <HarnessMark session={session} size={12} />
922+ <span className="truncate">{[session.actor, runsIn(session), repoName(session.repo, slug)].filter(Boolean).join(" · ")}</span>
923+ </p>
924+ <p className="mt-1 text-xs text-fg-soft tabular-nums" suppressHydrationWarning>
925+ {waited(Date.parse(session.startedAt), now)}
926+ {cost != null && Number.isFinite(cost) && <span className="text-faint"> · {money(Math.round(cost * 1_000_000))} so far</span>}
927+ {session.subagents > 0 && <span className="text-faint"> · {plural(session.subagents, "subagent")}</span>}
928+ </p>
929+ </div>
930+ </li>
931+ );
932+ }}
933+ />
934+ );
935+}
+5−155
11 /**
2− * The daily signal on Home (lib/signals.ts): the past week in four
3− * figures, each against the week before and drawn over ten, and the
4− * findings worth a look today, each a sentence with its evidence, where
5− * to see it, and a way to ask g1t about it.
2+ * The week's figures on the Workspace overview (lib/signals.ts), each
3+ * against the week before and drawn over ten, and the trend line they and
4+ * Home's week (components/home-week.tsx) draw with.
65 */
7−import { ChevronRight, MessageSquare, TriangleAlert } from "lucide-react";
8−import { type ReactNode, useId, useState } from "react";
6+import { useId, useState } from "react";
97 import { Link } from "react-router";
108
11−import { askG1t } from "../lib/ask";
129 import { cn } from "../lib/cn";
13−import type { DailySignal as DailySignalView, PulseTile, Signal } from "../lib/signals";
10+import type { PulseTile } from "../lib/signals";
1411 import { Skeleton } from "./ui/skeleton";
1512
1613 // --- A trend line -------------------------------------------------------------------
157154 <Skeleton className="h-6 w-16" />
158155 <Skeleton className="h-5 w-32" />
159156 </div>
160− ))}
161− </div>
162− );
163−}
164−
165−// --- The findings -------------------------------------------------------------------
166−
167−function Facts({ facts }: { facts: string[] }) {
168− return (
169− <p className="font-mono text-[0.75rem] leading-relaxed text-muted">
170− {facts.map((fact, i) => (
171− <span key={i}>
172− {i > 0 && <span className="px-1.5 text-faint">·</span>}
173− {fact}
174− </span>
175157 ))}
176− </p>
177− );
178−}
179−
180−function Actions({ signal }: { signal: Signal }) {
181− return (
182− <div className="flex flex-wrap items-center gap-x-4 gap-y-1">
183− {signal.link && (
184− <Link to={signal.link.to} className="inline-flex items-center gap-0.5 text-sm font-medium text-accent hover:underline">
185− {signal.link.label}
186− <ChevronRight size={14} />
187− </Link>
188− )}
189− <button type="button" onClick={() => askG1t(signal.ask)} className="inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
190− <MessageSquare size={12} />
191− Ask g1t
192− </button>
193158 </div>
194− );
195−}
196−
197−function Trend({ trend }: { trend: NonNullable<Signal["trend"]> }) {
198− const [point, setPoint] = useState<number | null>(null);
199− return (
200− <figure className="space-y-1.5">
201− <TrendLine values={trend.values} points={trend.points} label={`${trend.caption}, ${trend.range}`} width={196} height={40} onPoint={setPoint} />
202− <figcaption>
203− <p className="text-xs text-fg-soft">{trend.caption}</p>
204− <p className="font-mono text-[0.6875rem] text-faint">{point != null ? trend.points[point] : trend.range}</p>
205− </figcaption>
206− </figure>
207− );
208−}
209−
210−function Finding({ signal }: { signal: Signal }) {
211− return (
212− <li className={cn("grid gap-4 py-5 first:pt-0 last:pb-0", signal.trend && "md:grid-cols-[minmax(0,1fr)_12.5rem] md:gap-8")}>
213− <div className="min-w-0 space-y-2">
214− <h3 className="font-display text-lg leading-snug text-fg md:text-xl">{signal.headline}</h3>
215− <Facts facts={signal.facts} />
216− <Actions signal={signal} />
217− </div>
218− {signal.trend && (
219− <div className="md:pt-1">
220− <Trend trend={signal.trend} />
221− </div>
222− )}
223− </li>
224− );
225−}
226−
227−/** Findings under the lead, before "and N more". */
228−const SHOWN = 4;
229−
230−/**
231− * The daily signal: the day, the finding that matters most as the page's
232− * headline, and the others under it. A quiet week still gets a sentence.
233− * Null `view`: the work service did not answer.
234− */
235−export function DailySignal({ date, view, line, footer }: { date: string; view: DailySignalView | null; line?: ReactNode; footer?: ReactNode }) {
236− const [more, setMore] = useState(false);
237− const lead = view?.findings[0] ?? null;
238− const rest = view?.findings.slice(1) ?? [];
239− const shown = more ? rest : rest.slice(0, SHOWN);
240− return (
241− <section aria-labelledby="daily-signal" className="rounded-2xl border border-line bg-surface px-5 py-6 md:px-8 md:py-8">
242− <p className="font-mono text-[0.6875rem] tracking-[0.16em] text-accent uppercase">Daily signal</p>
243− <p className="mt-2 font-display text-xl text-fg-soft md:text-2xl">{date}</p>
244− {view == null ? (
245− <>
246− <h1 id="daily-signal" className="mt-4 font-display text-3xl leading-tight tracking-tight text-fg md:text-[2.5rem]">
247− Here&apos;s your day.
248− </h1>
249− <p className="mt-3 flex items-center gap-1.5 text-sm text-muted">
250− <TriangleAlert size={13} className="shrink-0 text-warn" />
251− What the work says couldn&apos;t be read just now, so today&apos;s findings aren&apos;t here.
252− </p>
253− </>
254− ) : (
255− <>
256− <h1
257− id="daily-signal"
258− className="mt-4 max-w-3xl font-display text-3xl leading-[1.12] tracking-tight text-balance text-fg md:text-[2.25rem] xl:text-[2.5rem]"
259− >
260− {lead?.headline ?? view.quiet}
261− </h1>
262− {lead && (
263− <div className={cn("mt-4 grid gap-4", lead.trend && "md:grid-cols-[minmax(0,1fr)_12.5rem] md:gap-8")}>
264− <div className="space-y-2">
265− <Facts facts={lead.facts} />
266− <Actions signal={lead} />
267− </div>
268− {lead.trend && <Trend trend={lead.trend} />}
269− </div>
270− )}
271− {!view.complete && (
272− <p className="mt-3 flex items-center gap-1.5 text-xs text-faint">
273− <TriangleAlert size={12} className="shrink-0" />
274− Some projects didn&apos;t answer, so these findings leave them out.
275− </p>
276− )}
277− </>
278− )}
279− {line && <p className="mt-4 text-sm text-muted">{line}</p>}
280− {shown.length > 0 && (
281− <>
282− <hr className="my-7 border-line" />
283− <ul className="divide-y divide-line">
284− {shown.map((signal) => (
285− <Finding key={signal.key} signal={signal} />
286− ))}
287− </ul>
288− {rest.length > SHOWN && (
289− <button type="button" onClick={() => setMore(!more)} className="mt-5 text-xs text-muted hover:text-fg">
290− {more ? "Show fewer" : `${rest.length - SHOWN} more ${rest.length - SHOWN === 1 ? "finding" : "findings"}`}
291− </button>
292− )}
293− </>
294− )}
295− {footer && <div className="mt-8 border-t border-line pt-6">{footer}</div>}
296− </section>
297− );
298−}
299−
300−export function DailySignalSkeleton({ date }: { date: string }) {
301− return (
302− <section className="rounded-2xl border border-line bg-surface px-5 py-6 md:px-8 md:py-8">
303− <p className="font-mono text-[0.6875rem] tracking-[0.16em] text-accent uppercase">Daily signal</p>
304− <p className="mt-2 font-display text-xl text-fg-soft md:text-2xl">{date}</p>
305− <Skeleton className="mt-5 h-9 w-full max-w-2xl" />
306− <Skeleton className="mt-2 h-9 w-2/3 max-w-xl" />
307− <Skeleton className="mt-5 h-3 w-64" />
308− </section>
309159 );
310160 }
+35−15
88 type WeekTotals,
99 agentStep,
1010 blockedSignal,
11+ dayBars,
1112 dayList,
1213 emptySignals,
1314 findings,
1819 pulse,
1920 quarterSpend,
2021 quarterStart,
21− quietHeadline,
2222 reviewSignal,
2323 reworkSignal,
2424 rhythm,
3333 spreading,
3434 stalledSignal,
3535 weekStarts,
36+ weekSummary,
3637 weekTotals,
3738 weeklySpend,
3839 } from "./signals.ts";
196197 ],
197198 };
198199 const signal = blockedSignal(ctx({ signals }))!;
199− assert.equal(signal.headline, "Three work items are waiting on one unreviewed change.");
200+ assert.equal(signal.headline, "tax#4821 holds up three work items, and no one has reviewed it yet.");
200201 assert.deepEqual(signal.facts, ["tax#4821 Nexus threshold configuration", "open 2 days", "holds up #8, #9, #10"]);
201202 assert.equal(signal.link!.to, "/acme/tax/pull/4821");
202203 assert.equal(blockedSignal(ctx()), null);
232233 ],
233234 });
234235 const signal = spendSignal(ctx({ signals, quarterUsage }))!;
235− assert.equal(signal.headline, "More than half of this quarter's agent spend is going into the Checkout replatform.");
236+ assert.equal(signal.headline, "The Checkout replatform took more than half of the agent spend this quarter.");
236237 assert.deepEqual(signal.facts, ["$28.3K of $52.4K", "quarter to date"]);
237238 const even = usage({ byPull: quarterUsage.byPull.map((slice) => ({ ...slice, micros: 1_000_000 })) });
238239 assert.equal(spendSignal(ctx({ signals, quarterUsage: even })), null);
251252 ],
252253 };
253254 const signal = shippedSignal(ctx({ signals }))!;
254− assert.equal(signal.headline, "Single sign-on shipped on Tuesday, three weeks ahead of schedule.");
255+ assert.equal(signal.headline, "Single sign-on closed Tuesday, three weeks ahead of schedule.");
256+ assert.equal(signal.day, "2026-10-13");
255257 assert.deepEqual(signal.facts, ["11 weeks in build", "2 changes", "2 projects"]);
256258 const old = { ...signals, milestones: [{ ...signals.milestones[0]!, closedAt: "2026-09-01T00:00:00Z" }] };
257259 assert.equal(shippedSignal(ctx({ signals: old })), null, "only the last week's");
298300 assert.equal(signal.headline, "bo is starting to work in web, which ana has carried almost alone.");
299301 });
300302
301−test("findings come highest first, and a quiet week still says something", () => {
303+test("findings come highest first, and the week is said in a sentence that claims nothing about what needs anyone", () => {
302304 const signals = {
303305 ...emptySignals(),
304306 waiting: [{ repoId: "r1", number: 2, title: "Fix", createdAt: "2026-10-16T00:00:00Z", agent: false, draftSeconds: null, item: 1, blocks: [3, 4] }],
308310 found.map((s) => s.key),
309311 ["blocked", "stalled"],
310312 );
311− assert.equal(quietHeadline(week("x")), "A quiet week: nothing has landed yet, and nothing is stuck.");
312− assert.equal(
313− quietHeadline(week("x", { merged: 4, agentMerged: 2 })),
314− "Four changes landed this week, 50% of them an agent's, and nothing needs a closer look.",
315− );
313+ assert.equal(weekSummary(week("x")), "Nothing has landed this week yet.");
314+ assert.equal(weekSummary(week("x", { merged: 4, agentMerged: 2 })), "Four changes landed this week, 50% of them by agents.");
316315 // A workspace that pushes straight to main: agents' direct commits are what landed.
317− assert.equal(
318− quietHeadline(week("x", { merged: 1, agentMerged: 0, agentCommits: 9 })),
319− "Ten changes landed this week, 90% of them an agent's, and nothing needs a closer look.",
320− );
316+ assert.equal(weekSummary(week("x", { merged: 1, agentMerged: 0, agentCommits: 9 })), "Ten changes landed this week, 90% of them by agents.");
321317 });
322318
323319 test("waits are said in words and in a few characters", () => {
441437 test("agents' direct commits count as what they landed, and as what spend is spread over", () => {
442438 const weeks = [week("2026-10-05", { merged: 2, agentMerged: 1, agentCommits: 3 }), week("2026-10-12", { merged: 1, agentMerged: 1, agentCommits: 9 })];
443439 const [changes, spend] = pulse({ slug: "acme", weeks, spend: [4_000_000, 20_000_000] });
444− assert.equal(changes!.label, "Agent-authored changes");
440+ assert.equal(changes!.label, "Landed by agents");
445441 assert.equal(changes!.value, "10");
446442 assert.deepEqual(changes!.delta, { text: "+6", tone: "good" });
447443 assert.equal(changes!.points[1], "Week of Oct 12: 1 merged, 9 pushed directly, of 10 changes");
448444 assert.equal(spend!.value, "$2.00", "$20 over ten agent changes");
449445 });
446+
447+test("the last two weeks day by day: every repository added up, agents' and people's, and empty days as zeros", () => {
448+ const bars = dayBars(
449+ [
450+ { day: "2026-10-18", merged: 3, agentMerged: 2, agentCommits: 4, sessions: 5 },
451+ { day: "2026-10-18", merged: 1, agentMerged: 0, agentCommits: 0, sessions: 1 },
452+ { day: "2026-10-10", merged: 2, agentMerged: 2, agentCommits: 0, sessions: 0 },
453+ // Outside the two weeks: left out.
454+ { day: "2026-09-01", merged: 9, agentMerged: 9, agentCommits: 9, sessions: 9 },
455+ ],
456+ NOW,
457+ );
458+ assert.equal(bars.length, 14);
459+ assert.equal(bars[0]!.day, "2026-10-05");
460+ assert.deepEqual(bars.at(-1), { day: "2026-10-18", agent: 6, people: 2, sessions: 6 });
461+ assert.deepEqual(
462+ bars.find((bar) => bar.day === "2026-10-10"),
463+ { day: "2026-10-10", agent: 2, people: 0, sessions: 0 },
464+ );
465+ assert.deepEqual(
466+ bars.find((bar) => bar.day === "2026-10-11"),
467+ { day: "2026-10-11", agent: 0, people: 0, sessions: 0 },
468+ );
469+});
+55−17
315315 return [
316316 {
317317 key: "merges",
318− label: "Agent-authored changes",
318+ label: "Landed by agents",
319319 value: String(agentChanges(now)),
320320 delta: deltaOf(agentChanges(now), before ? agentChanges(before) : null, count, true),
321321 series: weeks.map(agentChanges),
325325 },
326326 {
327327 key: "spend",
328− label: "Spend per agent change",
328+ label: "Cost per agent change",
329329 value: dollars(perMerge[at] ?? null),
330330 delta: deltaOf(perMerge[at] ?? null, before ? (perMerge[at - 1] ?? null) : null, (abs) => dollars(abs), false),
331331 series: perMerge,
335335 },
336336 {
337337 key: "stalled",
338− label: "Stalled changes",
338+ label: "Gone quiet",
339339 value: String(now.stalled),
340340 delta: deltaOf(now.stalled, before?.stalled ?? null, count, false),
341341 series: weeks.map((week) => week.stalled),
345345 },
346346 {
347347 key: "rework",
348− label: "Agent rework rate",
348+ label: "Sent back",
349349 value: percent(rework[at] ?? null),
350350 delta: deltaOf(rework[at] ?? null, before ? (rework[at - 1] ?? null) : null, (abs) => `${(abs * 100).toFixed(1).replace(/\.0$/, "")} pts`, false),
351351 series: rework,
369369 trend?: { values: (number | null)[]; points: string[]; caption: string; range: string };
370370 /** How much it matters today: findings are shown highest first. */
371371 weight: number;
372+ /** The UTC day it happened (`YYYY-MM-DD`), when it happened on one: Home pins it there on the week. */
373+ day?: string;
372374 /** What Ask is asked, to look into it. */
373375 ask: string;
374376 };
411413 const shown = top.blocks.slice(0, 3).map((number) => `#${number}`);
412414 return {
413415 key: "blocked",
414− headline: n === 1 ? "A work item is waiting on a change no one has reviewed." : `${countWord(n)} work items are waiting on one unreviewed change.`,
416+ headline:
417+ n === 1
418+ ? `${repo.name}#${top.number} holds up a work item, and no one has reviewed it yet.`
419+ : `${repo.name}#${top.number} holds up ${countWord(n).toLowerCase()} work items, and no one has reviewed it yet.`,
415420 facts: [
416421 `${repo.name}#${top.number} ${top.title}`,
417422 `open ${spoken(ago(ctx.now, top.createdAt))}`,
542547 if (!top) return null;
543548 const share = top.micros / total;
544549 if (share < 0.35 || groups.size < 2) return null;
545− const amount = share > 0.5 ? "More than half" : share > 1 / 3 ? "More than a third" : `${Math.round(share * 100)}%`;
550+ const amount = share > 0.5 ? "more than half" : share > 1 / 3 ? "more than a third" : `${Math.round(share * 100)}%`;
546551 return {
547552 key: "spend",
548− headline: `${amount} of this quarter's agent spend is going into ${top.label}.`,
553+ headline: `${top.label.charAt(0).toUpperCase()}${top.label.slice(1)} took ${amount} of the agent spend this quarter.`,
549554 facts: [`${dollars(top.micros)} of ${dollars(total)}`, "quarter to date"],
550555 link: { label: "See the spend", to: `/${ctx.slug}/-/workspace#spend` },
551556 weight: 25 + share * 40,
580585 }
581586 return {
582587 key: "shipped",
583− headline: `${recent.title} shipped on ${weekday}${schedule}.`,
588+ headline: `${recent.title} closed ${weekday}${schedule}.`,
589+ day: recent.closedAt!.slice(0, 10),
584590 facts: [
585591 `${weeksIn} ${weeksIn === 1 ? "week" : "weeks"} in build`,
586592 `${pulls.length} ${pulls.length === 1 ? "change" : "changes"}`,
607613 trend: {
608614 values: ctx.weeks.map((week) => week.stalled),
609615 points: ctx.weeks.map((week) => `Week of ${weekLabel(week.start)}: ${week.stalled}`),
610− caption: "Stalled changes",
616+ caption: "Gone quiet",
611617 range: `${ctx.weeks[0]!.stalled} → ${now.stalled} over ${ctx.weeks.length} weeks`,
612618 },
613619 weight: 30 + Math.min(25, more * 2),
815821 facts: [`${first.elsewhere} merges elsewhere before`, `first in ${repo.name}`],
816822 link: { label: "See the people", to: `/${ctx.slug}/-/workspace#quiet` },
817823 weight: 20 + Math.min(10, others * 3),
824+ day: first.firstMergedAt.slice(0, 10),
818825 ask: `What did ${first.name} change in ${repo.namespace}/${repo.name}?`,
819826 };
820827 }
835842 return found.sort((a, b) => b.weight - a.weight);
836843 }
837844
838−/** The headline when nothing stands out: still a sentence about the week. */
839−export function quietHeadline(week: WeekTotals | undefined): string {
845+/**
846+ * The week in a sentence: what landed and how much of it was agents'. It
847+ * says nothing about what needs anyone; Home leads with that on its own.
848+ */
849+export function weekSummary(week: WeekTotals | undefined): string {
840850 const landed = week ? allChanges(week) : 0;
841− if (!week || landed === 0) return "A quiet week: nothing has landed yet, and nothing is stuck.";
851+ if (!week || landed === 0) return "Nothing has landed this week yet.";
842852 const share = agentShare(week);
843− return `${countWord(landed)} ${landed === 1 ? "change" : "changes"} landed this week${share ? `, ${percent(share)} of them an agent's` : ""}, and nothing needs a closer look.`;
853+ return `${countWord(landed)} ${landed === 1 ? "change" : "changes"} landed this week${share ? `, ${percent(share)} of them by agents` : ""}.`;
854+}
855+
856+/** One day on Home's week: what landed, agents' and people's, and the sessions started. */
857+export type DayBar = { day: string; agent: number; people: number; sessions: number };
858+
859+/**
860+ * The last `count` UTC days to `now`, oldest first, every repository's
861+ * rows added up; a day with nothing in it is zeros. Agents' are their
862+ * merged pull requests and the commits their sessions pushed directly;
863+ * people's, the other merged pull requests.
864+ */
865+export function dayBars(days: Pick<SignalDay, "day" | "merged" | "agentMerged" | "agentCommits" | "sessions">[], now: number, count = SIGNAL_DAYS): DayBar[] {
866+ const until = utcDay(now);
867+ const bars = new Map<string, DayBar>();
868+ for (let i = count - 1; i >= 0; i--) {
869+ const day = addDays(until, -i);
870+ bars.set(day, { day, agent: 0, people: 0, sessions: 0 });
871+ }
872+ for (const row of days) {
873+ const bar = bars.get(row.day);
874+ if (!bar) continue;
875+ bar.agent += row.agentMerged + (row.agentCommits ?? 0);
876+ bar.people += Math.max(0, row.merged - row.agentMerged);
877+ bar.sessions += row.sessions;
878+ }
879+ return [...bars.values()];
844880 }
845881
846−/** What Home shows: the pulse, and the findings with the one that leads. */
882+/** What Home shows of the work: the week's figures, its days, and the findings, most important first. */
847883 export type DailySignal = {
848884 tiles: PulseTile[];
849885 findings: Signal[];
850− /** The headline when no finding stands out. */
851− quiet: string;
886+ /** The week in a sentence (`weekSummary`). */
887+ summary: string;
888+ days: DayBar[];
852889 complete: boolean;
853890 };
854891
874911 quarterUsage: data.quarterUsage,
875912 quarter: data.range.quarter,
876913 }),
877− quiet: quietHeadline(data.weeks[data.weeks.length - 1]),
914+ summary: weekSummary(data.weeks[data.weeks.length - 1]),
915+ days: dayBars(data.signals.days, data.now),
878916 complete: data.complete,
879917 };
880918 }
+124−142
11 import { Suspense, useEffect, useState } from "react";
22 import { Await, data } from "react-router";
33
4−import { hasCodeAccess } from "@g1t/contracts";
4+import { type WorkspaceSession, hasCodeAccess } from "@g1t/contracts";
55
66 import type { Route } from "./+types/home";
77 import {
88 Card,
99 LandedFeed,
10+ LiveSessions,
1011 NeedsList,
1112 PeopleSummary,
1213 RowsSkeleton,
1819 WindowSwitch,
1920 } from "../../components/home";
2021 import { HomeAsk, WhatChangedCard } from "../../components/home-ask";
21−import { DailySignal, DailySignalSkeleton, PulseSkeleton, PulseStrip } from "../../components/signals";
22+import { HomeWeek, HomeWeekSkeleton } from "../../components/home-week";
2223 import { Skeleton } from "../../components/ui/skeleton";
2324 import { sidebarOrNull } from "../../lib/chat.server";
2425 import {
6667 } from "../../lib/home.server";
6768 import { HOME_WATCH_MAX, type HomeRepo } from "../../lib/home-live";
6869 import { readCookie } from "../../lib/mission";
69−import { dailySignal, signalDate } from "../../lib/signals";
70+import { dailySignal } from "../../lib/signals";
7071 import { loadSignalData } from "../../lib/signals.server";
7172 import { useHomeLive } from "../../lib/use-home-live";
7273 import { guideIsFor, loadOnboarding } from "../../lib/onboarding.server";
7374 import { GettingStartedButton, OnboardingOnPage } from "../../components/onboarding";
7475 import { loadMissionControl } from "../../lib/mission-control.server";
7576 import { page } from "../../lib/meta";
76−import { inbox } from "../../lib/services.server";
77+import { inbox, work } from "../../lib/services.server";
7778 import { requireUser, roleIn } from "../../lib/session.server";
7879 import { homePagePath } from "../../lib/workspace-nav";
7980 import { Boundary } from "../../components/boundary";
8283 return page(args, { title: `Home · ${params.owner} · g1t` });
8384 }
8485
85−/** Unread notifications read for Needs you. */
86+/** Unread notifications read for Your move. */
8687 const NOTIFICATIONS_READ = 50;
88+/** Live sessions read for Live. */
89+const LIVE_READ = 12;
8790
8891 /**
89− * Home: the workspace's front page. First the past week in four figures
90− * and the daily signal: what the workspace's work says today, the finding
91− * that matters most as the page's headline, each with its evidence
92− * (lib/signals.ts), and under them a box that asks g1t about any of it
92+ * Home: the workspace's front page. First the box that asks g1t
9393 * (components/home-ask: What changed summarizes the span on the page, and
94− * Explain on each change that landed asks about it); then where you're
95− * needed now, with no time window; what happened since you were last here (or over the last 24
96− * hours or 7 days, `?window=`): what people did, from the events
97− * service's digest of the span and chat's, then what landed; what is
98− * running; what the span cost. The last visit is kept by notify, per
94+ * Explain on each change that landed asks about it). Then Your move:
95+ * everything waiting on you, with no time window, beside Live: the agents
96+ * at work, workflow runs and builds going now. Then the week's work
97+ * (components/home-week, lib/signals.ts): four figures, the last two weeks
98+ * day by day, and what is worth a look, each with its evidence and pinned
99+ * to its day where it has one. Then what happened since you were last
100+ * here (or over the last 24 hours or 7 days, `?window=`): what people
101+ * did, from the events service's digest of the span and chat's, then what
102+ * landed; beside it what the span cost and what settled by itself. The last visit is kept by notify, per
99103 * person and workspace, and marked by the page once it has been looked
100104 * at (routes/workspace/home-seen.ts), so a refresh never wipes it. Only
101105 * the last visit (for the span) is waited for; every section streams in
219223 missing: [...(code && (!workflows || !workflows.complete) ? ["Workflows"] : []), ...(code && !deploys ? ["Deployments"] : [])],
220224 }))
221225 .catch(() => ({ rows: [], missing: ["Workflows", "Deployments"] }));
226+ // The agents at work now: harness and hosted sessions that are live.
227+ const live: Promise<WorkspaceSession[] | null> = code
228+ ? work
229+ .listWorkspaceSessions(slug, viewer, { shown: "live", sessionsOnly: true, limit: LIVE_READ })
230+ .then((result) => (result.ok ? result.value.sessions : null))
231+ .catch(() => null)
232+ : Promise.resolve([]);
222233 const spend = loadSpend(viewer, slug, span).catch(() => null);
223234 // What the workspace's work says: the pulse and the findings, for members who see Code.
224235 const signal = code
247258 attention: waiting,
248259 away,
249260 running: runningNow,
250− /** The sentence under the heading needs the digest, what landed and what waits. */
251− head: Promise.all([digest, happened, waiting]),
261+ live,
262+ /** The sentence about the span needs the digest and what landed. */
263+ head: Promise.all([digest, happened]),
252264 spend,
253265 signal,
254− date: signalDate(now, tz),
255266 watch,
256267 onboarding,
257268 setupAsked,
316327 }
317328
318329 export default function Home({ loaderData }: Route.ComponentProps) {
319− const { slug, now, tz, span, digest, happened, attention: waiting, away, running: runningNow, head, spend, signal } = loaderData;
330+ const { slug, now, tz, span, digest, happened, attention: waiting, away, running: runningNow, live, head, spend, signal } = loaderData;
320331 useMarkSeen(slug, now);
321332 useHomeLive(slug, useWatched(loaderData.watch));
322333 return (
323− <div className="mx-auto w-full max-w-260 space-y-5 px-4 py-5 md:px-10 md:py-8">
334+ <div className="mx-auto w-full max-w-360 space-y-5 px-4 py-5 md:px-10 md:py-8">
324335 <OnboardingOnPage slug={slug} view={loaderData.onboarding} asked={loaderData.setupAsked} />
325336 {loaderData.guide && (
326337 <div className="flex justify-end">
327338 <GettingStartedButton slug={slug} variant="soft" />
328339 </div>
329340 )}
330− {/* 0. The past week, against the week before, for members who see Code. */}
341+ {/* 0. The box that asks g1t leads the page, above everything it can be asked about. */}
342+ <HomeAsk span={span} />
343+ {/*
344+ What needs you and what is going on now, side by side; on a phone,
345+ one after the other. Then the week's work, then what happened since
346+ you were last here beside what it cost.
347+ */}
348+ <div className="grid grid-cols-[minmax(0,1fr)] items-start gap-5 lg:grid-cols-[minmax(0,1fr)_24rem]">
349+ {/* 1. Your move: everything waiting on you, the one to start with on top, with no time window. */}
350+ <div id="needed" className="scroll-mt-4">
351+ <Suspense
352+ fallback={
353+ <Card title="Your move">
354+ <RowsSkeleton rows={4} />
355+ </Card>
356+ }
357+ >
358+ {/* Each section's data says what did not answer; a section that fails to draw keeps that to itself. */}
359+ <Await resolve={waiting}>
360+ {(att) => (
361+ <Card title="Your move" hint={waitingSentence(att.rows.length)} link={{ label: "Notifications", to: "/notifications" }}>
362+ <Boundary what="Your move">
363+ <NeedsList attention={att} now={now} />
364+ </Boundary>
365+ </Card>
366+ )}
367+ </Await>
368+ </Suspense>
369+ </div>
370+
371+ {/* 2. Live: agents at work, then workflow runs and builds, as they go. */}
372+ <Card title="Live" hint="Agents, runs and builds going now">
373+ <Suspense fallback={<RowsSkeleton rows={2} />}>
374+ <Await resolve={Promise.all([live, runningNow])}>
375+ {([sessions, value]) => (
376+ <Boundary what="Live">
377+ <LiveSessions sessions={sessions} slug={slug} now={now} />
378+ <RunningList rows={value.rows} missing={value.missing} now={now} sessions={sessions?.length ?? 0} />
379+ </Boundary>
380+ )}
381+ </Await>
382+ </Suspense>
383+ </Card>
384+ </div>
385+
386+ {/* 3. The week's work, for members who see Code: its figures, its days, and what is worth a look. */}
331387 {signal && (
332− <Suspense fallback={<PulseSkeleton />}>
388+ <Suspense fallback={<HomeWeekSkeleton />}>
333389 <Await resolve={signal}>
334− {(value) =>
335− value && (
336− <Boundary what="The past week">
337− <PulseStrip tiles={value.tiles} />
338− </Boundary>
339− )
340− }
390+ {(value) => (
391+ <Boundary what="This week">
392+ <HomeWeek view={value} />
393+ </Boundary>
394+ )}
341395 </Await>
342396 </Suspense>
343397 )}
344− {/*
345− Two columns on wide screens: what to do and what happened on the
346− left, read top to bottom; what is going on now and what it costs on
347− the right, kept in view. On a phone it is one column: Where you're
348− needed, Running now, what happened, then Spent and Settled (the side
349− column's cards join it, `contents`, each in its place by `order`).
350− */}
351− <div className="grid grid-cols-[minmax(0,1fr)] items-start gap-5 lg:grid-cols-[minmax(0,1fr)_20rem]">
352− {/* 1. Where you're needed now, with the one to start with on top: no window. */}
353− <div className="order-1 space-y-5 lg:col-start-1 lg:row-start-1">
354− {/* 1. The daily signal, then a box that asks g1t about any of it; the line under it is yours. */}
355− {(() => {
356− const line = (
357− <Suspense fallback={<Skeleton className="inline-block h-3.5 w-72 align-middle" />}>
358− <Await resolve={head}>
359− {([digest, happened, att]) => (
360− <>
361− <span className="text-fg-soft">{waitingSentence(att.rows.length)}</span> {spanSentence(span, digest, happened?.landed?.length ?? null)}
362− </>
363− )}
364− </Await>
365− </Suspense>
366− );
367− const ask = <HomeAsk span={span} compact />;
368− // Without Code there is no work to read: the box leads, as it always has.
369− if (!signal)
370− return (
371− <header className="space-y-3">
372− <HomeAsk span={span} />
373− <p className="mx-auto max-w-2xl text-center text-sm leading-relaxed text-muted">{line}</p>
374− </header>
375− );
376− return (
377− <Suspense fallback={<DailySignalSkeleton date={loaderData.date} />}>
378− <Await resolve={signal}>
379− {(value) => (
380− <Boundary what="The daily signal">
381− <DailySignal date={loaderData.date} view={value} line={line} footer={ask} />
382− </Boundary>
383− )}
384− </Await>
385− </Suspense>
386− );
387− })()}
388− <div id="needed" className="scroll-mt-4">
389− <Card
390− title="Where you're needed"
391− hint="Everything waiting on you, however long ago it started"
392− link={{ label: "Notifications", to: "/notifications" }}
393− >
394− <Suspense fallback={<RowsSkeleton rows={4} />}>
395− {/* Each section's data says what did not answer; a section that fails to draw keeps that to itself. */}
396− <Await resolve={waiting}>
397− {(att) => (
398− <Boundary what="Where you're needed">
399− <NeedsList attention={att} now={now} />
400− </Boundary>
401− )}
402− </Await>
403− </Suspense>
404− </Card>
405− </div>
406− </div>
407398
408− {/* 2. Now: what is running, what it cost, what settled by itself. */}
409− <aside aria-label="Now" className="contents lg:sticky lg:top-4 lg:col-start-2 lg:row-span-2 lg:row-start-1 lg:block lg:space-y-5">
410− <Card title="Running now" className="order-2">
411− <Suspense fallback={<RowsSkeleton rows={2} />}>
412− <Await resolve={runningNow}>
413− {(value) => (
414− <Boundary what="Running now">
415− <RunningList rows={value.rows} missing={value.missing} now={now} />
416− </Boundary>
417− )}
418− </Await>
419− </Suspense>
420− </Card>
421− <div className="order-4">
422− <Suspense
423− fallback={
424− <Card title="Spent">
425− <SpendSkeleton />
426− </Card>
427− }
428− >
429− <Await resolve={spend}>
430− {(value) => (
431− <Boundary what="Spent">
432− <SpendCard spend={value} slug={slug} span={span} />
433− </Boundary>
434− )}
435− </Await>
436− </Suspense>
437− </div>
438− <div className="order-5">
439− <Suspense
440− fallback={
441− <Card title="Settled for you">
442− <RowsSkeleton rows={1} />
443− </Card>
444− }
445− >
446− <Await resolve={away}>
447− {(value) => (
448− <Boundary what="Settled for you">
449− <SettledCard away={value} now={now} />
450− </Boundary>
451− )}
452− </Await>
453− </Suspense>
454− </div>
455− </aside>
456−
457− {/* 3. What happened in the span: what changed in plain English first, then who did what, then what landed. */}
458− <section aria-labelledby="since" className="order-3 rounded-2xl border border-line bg-surface lg:col-start-1 lg:row-start-2">
399+ <div className="grid grid-cols-[minmax(0,1fr)] items-start gap-5 lg:grid-cols-[minmax(0,1fr)_24rem]">
400+ {/* 4. What happened in the span: in a sentence, then in plain English, then who did what, then what landed. */}
401+ <section aria-labelledby="since" className="rounded-2xl border border-line bg-surface">
459402 <header className="flex flex-wrap items-center gap-x-3 gap-y-2 px-5 pt-4">
460403 <div className="flex min-w-0 flex-wrap items-baseline gap-x-3 gap-y-1">
461404 <h2 id="since" className="text-base font-semibold tracking-tight text-fg">
467410 <WindowSwitch span={span} />
468411 </div>
469412 </header>
413+ <p className="px-5 pt-2 text-sm text-muted">
414+ <Suspense fallback={<Skeleton className="inline-block h-3.5 w-72 align-middle" />}>
415+ <Await resolve={head}>{([digest, happened]) => spanSentence(span, digest, happened?.landed?.length ?? null)}</Await>
416+ </Suspense>
417+ </p>
470418 {/* Summarize leads: the plain-English account is what most people come for. */}
471419 <div className="mt-4">
472420 <WhatChangedCard key={span.key} slug={slug} span={span} tz={tz} />
506454 </Await>
507455 </Suspense>
508456 </section>
457+
458+ {/* 5. What the span cost, and what settled by itself: kept in view beside it. */}
459+ <aside aria-label="Cost and settled" className="space-y-5 lg:sticky lg:top-4">
460+ <Suspense
461+ fallback={
462+ <Card title="Spent">
463+ <SpendSkeleton />
464+ </Card>
465+ }
466+ >
467+ <Await resolve={spend}>
468+ {(value) => (
469+ <Boundary what="Spent">
470+ <SpendCard spend={value} slug={slug} span={span} />
471+ </Boundary>
472+ )}
473+ </Await>
474+ </Suspense>
475+ <Suspense
476+ fallback={
477+ <Card title="Settled for you">
478+ <RowsSkeleton rows={1} />
479+ </Card>
480+ }
481+ >
482+ <Await resolve={away}>
483+ {(value) => (
484+ <Boundary what="Settled for you">
485+ <SettledCard away={value} now={now} />
486+ </Boundary>
487+ )}
488+ </Await>
489+ </Suspense>
490+ </aside>
509491 </div>
510492 </div>
511493 );

Comments

Sign in to comment.