Skip to content

teams page: show a team's total live cost, as a figure of its own #1878

Open

Description

@santoshkumarradha

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 in internal/tui3/teamcrew.go, dropSpend).
  • Its read model is TeamSpend / TeamSpendIn (internal/teams/spend.go): the machine ledger <home>/v3/usage.jsonl folded for one local day, a line counted when its session or its root is a member of the team or of a team under it. Spend.ByMember is 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 --host door (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

  1. 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.
  2. 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 session or its root is a conversation in the subtree — and a line whose root is 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 / FormerManager exist already and File.RemoveMember (internal/teams/teams.go) already fills them, so a member that has left is known. What is missing is LeftAt: Member.JoinedAt bounds 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 today cap 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.
  3. 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, and TeamSpendStamp the page's clock asks same to)
  • internal/session/team_cap.go (teamSpendOf) and internal/remote/delegation.go — the cap gate and the --host door share the read model
  • internal/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.json with one team, a manager, one member and one former member, and a fixture ledger whose lines cover each of them plus a line whose root is a delegated task — the header for the selected team reads the exact total (e.g. $67.46 total, spelled as Money spells it) and the team's rail row reads the same figure. Driven the way TestTeamsHeaderUsesTodaysRaisedCap (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 root is 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, and internal/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

  1. added
    featureWork that adds a capability; developers break it into tasks
    area:chatThe v3 surface a person sits in front of (internal/tui3)
    needs groomingShape and scope are still being decided — the approach is not settled
    on Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:chatThe v3 surface a person sits in front of (internal/tui3)featureWork that adds a capability; developers break it into tasksneeds groomingShape and scope are still being decided — the approach is not settled

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions