Repository navigation
teams page: show a team's total live cost, as a figure of its own #1878
Description
Summary
The teams page should say what a team has cost — all of it: the manager, every conversation in the team, and everything those conversations delegated. One live figure of the team's own, drawn on its rail row and in the pane header, kept apart from what a single chat cost and from the cap reading that is drawn there today.
Current behavior
- The page draws exactly one money figure per team, and it is a cap reading rather than a total:
$1.20 of $5 today, dim, in the header strip (teamsSpendWords,internal/tui3/teamspagedraw.go; placed as a header piece by the crew line ininternal/tui3/teamcrew.go,dropSpend). - Its read model is
TeamSpend/TeamSpendIn(internal/teams/spend.go): the machine ledger<home>/v3/usage.jsonlfolded for one local day, a line counted when itssessionor itsrootis a member of the team or of a team under it.Spend.ByMemberis computed and never drawn. - The Spend place (
internal/tui3/place_spend.go) answers by day, by model and by subject — subjects are conversations, never teams. - The same read model is the cap gate (
teamSpendOf,internal/session/team_cap.go) and the--hostdoor (internal/remote/delegation.go), so "what counts as a team's money" is one decision in one place and cannot be changed in the page alone.
Why it matters
Three ways that figure is not the team's cost. Measured on one real team, over one day of that team's work, the ledger's lines read like this:
| what the money was | cost |
|---|---|
| the team's conversations, their own calls | $61.30 |
the task conversations the team's delegates ran in (root = a member) |
$4.16 |
| members that have since left the team | $1.99 |
Altogether $67.46 (each line rounded to the cent, so together they read a cent under). TeamSpend returns $65.47 for that day — the members who left are not members any more, so their share sits inside no team — and $0.00 the next morning, so a person asking the page what that team cost gets nothing at all a day later. When it does answer, it answers "against a cap", which is a different question from "what did this cost me", and it answers once per team in a header, with no per-member split and nothing that separates the team's money from its chats'.
The figures came off the machine that filed this; reproducing them is not part of the work below.
Ask
- A team total, live, on the teams page — on each team's rail row and in the pane header for the selected team — spelled by
Money/RoundMoney(internal/teams/money.go) like every other figure on the surface. - The definition, decided here rather than left to whoever writes it. My pick:
- The subtree, not just the team. A team's total includes every team under it; the root team's total is every team's. This is the pool rule the cap already uses.
- Whole conversation chains. A line belongs to the team when its
sessionor itsrootis a conversation in the subtree — and a line whoserootis itself one of those delegated conversations (root= a task, not a member) counts too. Today's read is one hop only, so a task's own sub-task falls out of every figure. - Money follows the conversation, not the team record. A line belongs to the team whose subtree held its conversation when the line was written.
Team.FormerMembers/FormerManagerexist already andFile.RemoveMember(internal/teams/teams.go) already fills them, so a member that has left is known. What is missing isLeftAt:Member.JoinedAtbounds a membership from below only. Write it at removal and the rule is exact — a line counts when it falls inside one of the conversation's membership intervals in that subtree. The store keeps unknown fields whole, so the field is compatible in both directions. - All time, and today. The total runs from the team's
made. The existing$1.20 of $5 todaycap reading stays where it is; it does not become the total. The fold already reads every line of the ledger and keeps them by day (spendMemory.refresh), so an all-time total is the same fold no longer cut at a day — it costs no extra read. - A conversation in two teams counts in both — it did that team's work — and once in the root team's total.
- Whether the per-member split (
ByMember, already computed) is drawn beside the total. It is the line that shows one runaway member, and it is the reason the model computes it.
Grooming owes 2's five bullets and 3, and one placement question: the header drops pieces on a narrow line (teamcrew.go's keep loop), so the total may belong on the rail row and in the team's Settings card rather than in the strip.
Grooming: what the lifecycle does to the money
The acts exist — internal/teams/lifecycle.go and teams.go are the vocabulary — so the money rule has to answer for each of them. Disband closes a team and its descendants and keeps the rosters, and a member whose home team went closed turns Independent. Reopen opens it again. Delete disbands and then drops the records and <profile>/teams/<id>; conversations survive. RemoveMember moves a member to FormerMembers, and the manager to FormerManager. SetHome moves the reporting membership.
| the act | what the team's total does | why |
|---|---|---|
| a member is removed | keeps their lines from while they were there, stops counting the ones after | they were the team's then; LeftAt marks the end |
| a member moves to another team | the old team keeps their time in it; the new team counts from its JoinedAt |
one rule, two intervals, and no line counted twice on one page |
| the team is disbanded | the total stays, drawn on its card beside disbanded, and still counts toward its parent's total |
the code already takes that line for the cap — "a sub-team closed today still counts toward its parent's day" — and disbanding spends nothing more |
| a disbanded team is reopened | nothing moves | the figure was never stored, it is derived at read time |
| the team is deleted | its total leaves the page, and its conversations' money sits in no team's total afterwards, because becoming Independent rehomes them to nobody |
nothing is drawn on a record that is gone. This is the one act that forgets money, and the page should not pretend otherwise; the Spend place still carries each conversation's own figure |
| a sub-team is reparented | its money moves, retroactively, into the new chain's totals | the figure is derived from the tree as it stands |
Two consequences need a ruling in the same pass:
- A conversation in several teams counts in each of them, so team totals do not sum to a machine total. The teams page must never print the sum of team totals as though it were the machine's spend; that number belongs to the Spend place.
- The root team's total counts each line once and is the nearest thing to "all the money my teams spent" — not "what this machine spent".
Where
internal/teams/spend.go—TeamSpend,TeamSpendIn,Spend, and the day fold (spendMemory.refresh)internal/tui3/teamspagedraw.go(teamsSpendWords),internal/tui3/teamcrew.go(the header pieces)internal/tui3/teamspage.go(teamsPage.spend,spendStamp),internal/tui3/teamseam.go(TeamSeam.Spend, andTeamSpendStampthe page's clock askssameto)internal/session/team_cap.go(teamSpendOf) andinternal/remote/delegation.go— the cap gate and the--hostdoor share the read modelinternal/manual/chat/conversations-and-teams.md,internal/manual/chat/models-and-cost.md
Acceptance
- e2e: through the teams page's own keys, on a fixture profile — a
teams.jsonwith one team, a manager, one member and one former member, and a fixture ledger whose lines cover each of them plus a line whoserootis a delegated task — the header for the selected team reads the exact total (e.g.$67.46 total, spelled asMoneyspells it) and the team's rail row reads the same figure. Driven the wayTestTeamsHeaderUsesTodaysRaisedCap(internal/tui3/teamspage_test.go) drives the page today. - e2e: the control — the same fixture read on a later day keeps the same total while its cap words read
$0 today. The two figures must not be the same reading. - Unit: the fold — a line whose
rootis a delegated task two hops down counts; a line from a conversation in no team counts nowhere; a team under another is inside its parent's total and inside its own; a conversation in two teams is counted in both. - Unit, the lifecycle: a line written after a member was removed is not in the team's total; a member moved between teams is counted once on each team's page and never twice on one; a deleted team's money is in no total; the root team counts a line held by two teams once; a disbanded team's total is unchanged by the disbanding and still inside its parent's.
- The manual page that covers this (
internal/manual/chat/conversations-and-teams.md) quotes the new wording, andinternal/teams/spend.go's read-model comment is updated where it says what a team's spend is — the cap check must keep meaning what it means today. The comment also says a team's spend is one day's; that sentence is what this issue changes.
Not in scope: enforcing anything against the total. The cap stays a per-day ceiling; this is a reading.
Drafted with CodeAF · reviewed and owned by the author
Activity