Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
169 commits
Select commit Hold shift + click to select a range
1cbe5a3
tui3: an overview of open conversations
agentfield-bot Sep 23, 2026
0f00583
tui3: spaces live in the profile on an ordinary launch, and the new c…
agentfield-bot Sep 23, 2026
d01c05a
tui3: polish the conversations view painting
agentfield-bot Sep 23, 2026
34c8b3a
Merge branch 'task/wall-polish-paint' into feat/conversation-overview
agentfield-bot Sep 23, 2026
145b876
tui3: the wall opens on one press, scrolls by row and moves a little
agentfield-bot Sep 23, 2026
f48af81
Merge branch 'task/wall-polish-paint' into task/wall-polish-motion
agentfield-bot Sep 23, 2026
a0f2f06
Merge branch 'feat/conversation-overview' into task/wall-polish-motion
agentfield-bot Sep 23, 2026
b85bbc8
tui3: the wall tells the painter the pointer's row, and the hit helpe…
agentfield-bot Sep 23, 2026
767b9fb
tui3: the dock's signal test holds its flags on watchers nothing else…
agentfield-bot Sep 23, 2026
005602b
tui3: the wall's tile actions are labelled words, ? is help, and the …
agentfield-bot Sep 24, 2026
1f12208
tui3: spaces are called teams, in the code and on screen
agentfield-bot Sep 24, 2026
33fd9de
tui3: teams are known by id, can nest, and move from spaces.json to t…
agentfield-bot Sep 24, 2026
1ef4908
tui3: a new team is named once, from its conversations
agentfield-bot Sep 24, 2026
d4513fe
tui3: a conversation started while a team is shown joins it
agentfield-bot Sep 24, 2026
52abfb4
tui3: the strip's team chip is a switcher
agentfield-bot Sep 24, 2026
62393f4
manual: conversations and teams have a page of their own
agentfield-bot Sep 24, 2026
22cb944
tui3: the frame dumps show the naming card and the team switcher
agentfield-bot Sep 24, 2026
bc7eb55
session: ProposeTeams suggests teams for a person's conversations
agentfield-bot Sep 24, 2026
d8e3545
tui3: Organize suggests teams on the wall's All view
agentfield-bot Sep 24, 2026
c520c36
manual: Organize on the conversations and teams page
agentfield-bot Sep 24, 2026
e97e24e
docs: the design of conversations, teams, organize and the manager
agentfield-bot Sep 24, 2026
03bc2bb
teams: one store for teams, handles, the manager, Traffic and the digest
agentfield-bot Sep 24, 2026
3a11c71
tui3: teams are read and written through internal/teams
agentfield-bot Sep 24, 2026
810ba39
tui3: every teams edit is the store's read-modify-write
agentfield-bot Sep 24, 2026
d6cf54a
tui3: the manager's place on the strip, the switcher and the wall
agentfield-bot Sep 24, 2026
5fee54b
tui3: the Traffic rail beside the manager, and the manager's stop and…
agentfield-bot Sep 24, 2026
2061864
tui3: test that the Traffic read titles a member that joined untitled
agentfield-bot Sep 24, 2026
e0a922f
manual: the team manager page
agentfield-bot Sep 24, 2026
89f76c3
session: the team manager's half, through internal/teams
agentfield-bot Sep 24, 2026
92ac178
docs: making a manager, its screen, and the design as built
agentfield-bot Sep 24, 2026
edf474d
tui3: the manager's screen, reworked from the UX review
agentfield-bot Sep 24, 2026
6854558
session: team events, the brief as the manager's, and the start's card
agentfield-bot Sep 24, 2026
1436801
teams: read Traffic backwards for a tail and by binary search for a page
agentfield-bot Sep 24, 2026
6f6f721
teams: coarse ages in the digest so its note lands only on a real change
agentfield-bot Sep 24, 2026
bfce415
session: cache member journals by stamp, reuse the teams file, save c…
agentfield-bot Sep 24, 2026
2288c04
tui3, session: the started member wakes once, and the brief is a card
agentfield-bot Sep 24, 2026
2e42d28
docs: the manager's screen as reworked, its keys, and the change note
agentfield-bot Sep 24, 2026
5e49e5b
tui3: the wall reads the front only when it moved, and held tiles on …
agentfield-bot Sep 24, 2026
8d56e8c
tui3: the hint line names a Traffic row's handles whole
agentfield-bot Sep 24, 2026
26ef299
teams: stamps, a compare-and-swap change, and a Traffic reader that s…
agentfield-bot Sep 24, 2026
4f47f3b
remote: the teams doors, answered from the engine's profile
agentfield-bot Sep 24, 2026
ef6323d
tui3: teams and Traffic through a seam, written and read off the loop
agentfield-bot Sep 24, 2026
8f3db6e
codeaf: --host hands the surface the engine's teams
agentfield-bot Sep 24, 2026
ddbcbfb
docs: teams and the manager over --host, against the engine's store
agentfield-bot Sep 24, 2026
25ff869
remote: the wall's team naming and Organize proposals cross the wire
agentfield-bot Sep 24, 2026
2a4746c
tui3: Organize's Undo test compares against the list as written
agentfield-bot Sep 24, 2026
697ae25
tui3: team naming and Organize are asked beside the ordered line, wit…
agentfield-bot Sep 24, 2026
de9af8b
session: a conversation on the ordinary launch finds its teams
agentfield-bot Sep 24, 2026
1bf43fd
session: a manager is told it is one on its first request, as codeaf'…
agentfield-bot Sep 24, 2026
5153049
teams: a derived handle names what the conversation is about
agentfield-bot Sep 24, 2026
2bfb920
manual: the manager is told it is one, and how a handle is made
agentfield-bot Sep 24, 2026
214d905
teams: delegation store: settings with provenance, homes, LCA, lifecy…
agentfield-bot Sep 24, 2026
158fa1b
teams: gofmt the delegation store
agentfield-bot Sep 24, 2026
469ae13
remote, tui3, codeaf: the delegation doors cross --host
agentfield-bot Sep 24, 2026
e94db86
teams, config: the global manager is a real root team; team defaults …
agentfield-bot Sep 24, 2026
0965ed8
teams: Organize's quiet teams, from Traffic, packets and member writes
agentfield-bot Sep 24, 2026
33c28e7
docs: section 8, delegation: rulings, the exact store and wire API, t…
agentfield-bot Sep 24, 2026
653ca39
teams: a team carries a wake switch, on unless it says wake false
agentfield-bot Sep 24, 2026
dab811e
session: team traffic wakes an idle conversation: a directive its mem…
agentfield-bot Sep 24, 2026
edeea72
engine: open a team conversation nobody holds in its folder's host so…
agentfield-bot Sep 24, 2026
3ee8af9
docs: directives wake members, replies wake the manager, notes wait
agentfield-bot Sep 24, 2026
daedddf
tui3: the strip and the wall show what is open in this window, and of…
agentfield-bot Sep 24, 2026
0216b48
session, teams: a handle is one word the title model chooses
agentfield-bot Sep 24, 2026
d2db4b1
tui3: every team reference in a chat is a door
agentfield-bot Sep 24, 2026
dce2184
docs, manual: the view is what is open here, handles are one chosen w…
agentfield-bot Sep 24, 2026
221baa4
tui3: shorter hints for Open them and a member's link
agentfield-bot Sep 24, 2026
873779a
teams: a team carries a wake switch, on unless it says wake false
agentfield-bot Sep 24, 2026
c167ea7
session: team traffic wakes an idle conversation: a directive its mem…
agentfield-bot Sep 24, 2026
24e5a98
engine: open a team conversation nobody holds in its folder's host so…
agentfield-bot Sep 24, 2026
983dc13
docs: directives wake members, replies wake the manager, notes wait
agentfield-bot Sep 24, 2026
6f8dc18
session, config: wake is an inherited team setting
agentfield-bot Sep 24, 2026
8204739
teams: packet rotation, cap facts, answered lines, wrap-up marker, ac…
agentfield-bot Sep 24, 2026
0b94ca8
session: delegation: home routing, questions up as packets, caps, wra…
agentfield-bot Sep 24, 2026
95b2a54
remote: the wrap-up's two doors cross --host
agentfield-bot Sep 24, 2026
d02a5f8
docs: delegation as the session built it: routing, questions, packets…
agentfield-bot Sep 24, 2026
cdf6883
tui3: teams place after home, with its rail, inbox and hosted manager
agentfield-bot Sep 24, 2026
ef9198a
tui3: team card from the wall and switcher, close instead of delete, …
agentfield-bot Sep 24, 2026
3b11798
tui3: teams page tests for the card, closing, the Closed fold, member…
agentfield-bot Sep 24, 2026
b10da12
tui3: teams page reads better at every width
agentfield-bot Sep 24, 2026
7ae9d32
tui3, codeaf: teams page on the session's delegation contract
agentfield-bot Sep 24, 2026
180d673
manual, design: the teams page, its keys, and where it departs from t…
agentfield-bot Sep 25, 2026
7bd2e4c
tui3: @ completion offers teams and conversations beside files
agentfield-bot Sep 25, 2026
2ae98cc
session: a sent team or chat mention carries a bounded digest
agentfield-bot Sep 25, 2026
e2ddba9
docs, manual: @ names a team or a conversation as well as a file
agentfield-bot Sep 25, 2026
ab104ec
session: team_start honours a team's auto-wake being off
agentfield-bot Sep 25, 2026
0341dd6
session: an asking member goes idle when its process is gone
agentfield-bot Sep 25, 2026
f9f3553
session: a handle ask retries once on a timeout or a dropped connection
agentfield-bot Sep 25, 2026
3d040e2
teams: nesting in the store: rulings reach every party, the root seat…
agentfield-bot Sep 24, 2026
292fb78
session: nested teams: sub-teams, team_raise, one-level orders, the g…
agentfield-bot Sep 25, 2026
e719266
cmd/codeaf, remote: a sibling sub-team conflict ruled end to end, and…
agentfield-bot Sep 25, 2026
f057796
docs: nesting as the session built it: sub-teams, conflicts, one leve…
agentfield-bot Sep 25, 2026
f5a3cc8
tui3: the teams page brings its manager into the pane on every road, …
agentfield-bot Sep 25, 2026
51ebecf
docs: the teams page's manager pane says why when it cannot open, and…
agentfield-bot Sep 25, 2026
2e0b778
teams: where a team may move, and what a move changes
agentfield-bot Sep 25, 2026
4979e67
tui3: nesting on the teams page, and a one-line team header
agentfield-bot Sep 25, 2026
0776be6
teams: a message to several members is one entry, and an answer names…
agentfield-bot Sep 24, 2026
803b70a
session: a manager's question to several is one entry, and a member's…
agentfield-bot Sep 24, 2026
c83ff2b
tui3: the Traffic rail is threads, the newest at the top
agentfield-bot Sep 24, 2026
f2a2f1f
tui3: a manager's team_send is a thread card, and a member's chat mir…
agentfield-bot Sep 24, 2026
a2f9ccf
tui3: a manager's question is never folded away, and a narrow rail co…
agentfield-bot Sep 24, 2026
91562af
docs, manual: Traffic and threads, what the manager and members see
agentfield-bot Sep 24, 2026
755143c
tui3: the tab strip reads Home, the team chip, the manager, then the …
agentfield-bot Sep 24, 2026
08938b7
tui3: a chats word on the places bar goes back to the conversation
agentfield-bot Sep 25, 2026
ef47a9f
tui3: one top bar, the places bar takes the chat strip's geometry
agentfield-bot Sep 25, 2026
3287f0a
tui3: with the manager in front the right column is the traffic
agentfield-bot Sep 25, 2026
3f0c9a1
tui3: a handle on the Traffic opens the member at the message
agentfield-bot Sep 25, 2026
3bba987
tui3: the manager's tasks header says its way back in the hint line
agentfield-bot Sep 25, 2026
bcbf1a0
manual, tui3: no em dash on the lines the place renumbering touched
agentfield-bot Sep 25, 2026
ca13f5c
manual, design: moving teams, the members card, and the one-line team…
agentfield-bot Sep 25, 2026
788d2f3
session: the wake tests read the delivered brief, not its wording
agentfield-bot Sep 25, 2026
3454b5b
codeaf: the engine wake tests look for the directive, not its wording
agentfield-bot Sep 25, 2026
8023a1e
Merge #1429 (feat/conversation-overview 3454b5b65) into the delegatio…
agentfield-bot Sep 25, 2026
e427c12
tui3: the hosted inbox leaves the manager room, and a card says whose…
agentfield-bot Sep 25, 2026
40926ed
tui3: the wall's hint is said whole, and the doors say where teams are
agentfield-bot Sep 25, 2026
b09b282
tui3: a manager's steps are captioned as team work
agentfield-bot Sep 25, 2026
101e60f
tui3: the dock under the box says chats, and a hover says where a pre…
agentfield-bot Sep 25, 2026
e57e1e9
Merge #1429 (feat/conversation-overview 101e60ff8): the dock explains…
agentfield-bot Sep 25, 2026
9440fbb
tui3: a team name in a chat opens the teams page.
agentfield-bot Sep 25, 2026
109a95d
tui3: the jump to the teams page forgets the chat link's hover
agentfield-bot Sep 25, 2026
3cdded1
tui3: the tab strip's home and teams words are lowercase like the places
agentfield-bot Sep 25, 2026
714d6ee
tui3: the places move onto the wordmark row, the strip is on every page
agentfield-bot Sep 25, 2026
98c32c0
session: ReadRows reads named conversations without walking the machine
agentfield-bot Sep 25, 2026
78de102
tui3: the teams page reads its members, never drops a read, opens a m…
agentfield-bot Sep 25, 2026
7832c61
design: how the teams page loads, and one manager open at a time
agentfield-bot Sep 25, 2026
a7c9d4b
manual: the places on the top line, the chat strip on every page
agentfield-bot Sep 25, 2026
bc91204
Merge branch 'task/top-nav' into task/merge8
agentfield-bot Sep 25, 2026
5e34961
Record a committed team move in each affected team's Traffic.
agentfield-bot Sep 25, 2026
51cdbe6
teams: keep a wrap-up's start and bound in teams.json so a restart re…
agentfield-bot Sep 25, 2026
f4001c1
teams: raise one cap packet per crossing, across processes
agentfield-bot Sep 25, 2026
540cd17
tui3: a wrapped /help row keeps the column its sentence started in.
agentfield-bot Sep 25, 2026
b53d635
tui3: hint lines say esc cancel, the form the rest of them use.
agentfield-bot Sep 25, 2026
67cb26b
tui3: a renamed team updates the run label and the composer at once.
agentfield-bot Sep 25, 2026
34aaf30
tui3: one side column in every chat, Tasks and Traffic under one header
agentfield-bot Sep 25, 2026
70e56b9
tui3: the dock's door is `▦ All`, one button, as the strip spells it
agentfield-bot Sep 25, 2026
de70f78
tui3: the count that wants you never leaves the top line
agentfield-bot Sep 25, 2026
11e112a
tui3: the Teams settings tab edits the far machine over --host.
agentfield-bot Sep 25, 2026
5711d6d
manual, design: the one side column: header words, needs-you band, gr…
agentfield-bot Sep 25, 2026
c1c359f
Merge branch 'task/side-column' into task/merge8
agentfield-bot Sep 25, 2026
2efc73a
tui3: a rename clears the side column's Traffic rows, not the retired…
agentfield-bot Sep 25, 2026
67a676a
tui3: a word of the head under the pointer outranks the away column's…
agentfield-bot Sep 25, 2026
fcdf4dc
tui3: the strip's pieces stand one gap apart, and every door on it sp…
agentfield-bot Sep 25, 2026
36ba144
Merge branch 'task/merge8' into task/ux8
agentfield-bot Sep 25, 2026
e5ecc05
session: a busy decisions file leaves the wrap-up due.
agentfield-bot Sep 25, 2026
4db85dd
tui3: a swap lock on a held manager is not a refusal.
agentfield-bot Sep 25, 2026
c1af5c8
tui3: a narrow side column spends its cells on what the work is
agentfield-bot Sep 25, 2026
24d0a9c
Merge branch 'task/merge8' into task/ux8
agentfield-bot Sep 25, 2026
fbb8e81
tui3: the settings note over --host follows the tab.
agentfield-bot Sep 25, 2026
e0d4f9b
tui3: an untitled chat on home uses the sessions place's name
agentfield-bot Sep 25, 2026
51df815
tui3: a resize drops a hover the new layout may not draw
agentfield-bot Sep 25, 2026
0a088ea
tui3: every Traffic row reads who it is from and who it is for.
agentfield-bot Sep 25, 2026
1d76628
tui3: the chat tab strip is drawn only inside a chat
agentfield-bot Sep 25, 2026
3d2d654
standing: a proposal card names its kind, and each button says its ou…
agentfield-bot Sep 25, 2026
10be52e
session: a person's card answer is on the page the completion check r…
agentfield-bot Sep 25, 2026
65750de
session: a just-once card tells the model to do the step and stop
agentfield-bot Sep 25, 2026
ccc1f7a
session: the card-answer test drives the measured case, a repeating c…
agentfield-bot Sep 25, 2026
c6fabef
Merge origin/dev into feat/conversation-overview
agentfield-bot Sep 25, 2026
d6d896b
tui3: the teams surfaces ask the glyph door for their marks
agentfield-bot Sep 25, 2026
1aa24a5
tui3: the home strip tests read the one top bar's lead
agentfield-bot Sep 25, 2026
07a8df0
Merge feat/conversation-overview (dev merged in) into feat/team-deleg…
agentfield-bot Sep 25, 2026
3061dab
tui3: a run's parts on the side column are one line each, through the…
agentfield-bot Sep 25, 2026
517ab1f
tui3: a task page's parts and the teams page's spark use the merged r…
agentfield-bot Sep 25, 2026
f783558
tui3: dev's home-position test asks the nav, where home stands on thi…
agentfield-bot Sep 25, 2026
6416a6c
tui3: the one top bar test finds the strip's home by dev's lowercase …
agentfield-bot Sep 25, 2026
fec4af7
manual: home's highlighted-heading section answers which section the …
agentfield-bot Sep 25, 2026
25543e0
tui3: the search and rewind hints say esc clear, as the majority of h…
agentfield-bot Sep 25, 2026
3bcceba
Merge origin/feat/conversation-overview (6416a6cf9) into feat/team-de…
agentfield-bot Sep 25, 2026
6596524
tui3: dev's run-row tests read the side column's one line, a task pag…
agentfield-bot Sep 25, 2026
af36b0b
tui3: a Traffic row shows how long ago, and a press opens that message.
agentfield-bot Sep 25, 2026
32c0d24
changes: note for #1500
agentfield-bot Sep 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
24 changes: 24 additions & 0 deletions cmd/codeaf/chatv3.go
Original file line number Diff line number Diff line change
Expand Up @@ -1979,25 +1979,40 @@ func v3PolicyMode(workspace, profileDir, mode string) (*approval.Policy, error)
// - Pure reads of this machine — read, grep, find, ls. Nothing here changes a
// file, so the blanket mode's prompt is free to land where it matters, on
// bash, edit and write.
//
// - jobs, whose list and output are reads of processes the person already
// started. Its kill is not on the floor: it inherits the blanket mode,
// which asks.
//
// - The agent's own bookkeeping — remember, track and recall. These write to
// and read from the memories and working state it keeps for itself
// (internal/session's memory.go and state.go); no hand outside this process
// reads them, and asking somebody to approve the agent writing itself a
// reminder is asking about the wrong thing.
//
// - manual, which reads pages compiled into this binary and touches no disk
// at all (internal/session's tools_manual.go). A person who asks "what can
// you do" and is answered with a permission prompt has been asked to
// approve the program looking up its own documentation.
//
// - settings, which reads the person's own settings rows back through the
// registry (internal/session's tools_settings.go). It is manual's shape one
// file over — the answer to "what is my daily budget" is a lookup, and the
// credential rows read MASKED through the registry itself
// ([config.Setting.Secret]), so there is nothing here a prompt would be
// protecting.
//
// - The team verbs that stay inside a team the person made
// (internal/session's tools_team.go): a manager reading its members'
// states and pages, sending them a line, ending a member's turn the way
// the person's own Stop does, and a member posting to its room. Every one
// of them is a line in the team's own traffic log, which the person watches
// on the manager's rail, and none of them reaches a permission prompt.
//
// team_start is DELIBERATELY NOT HERE. It opens a new conversation that spends
// money for as long as it runs, so the blanket mode asks, the way it asks about
// propose_task.
//
// commit is DELIBERATELY NOT HERE, and it is the interesting half of the split.
// It is the fifth hand on the same working state, but it is the only one that
// declares a tracked subgoal FINISHED, and a session that can mark its own work
Expand All @@ -2020,6 +2035,15 @@ func v3BuiltinApprovals() map[string]any {
"jobs": "allow",
"remember": "allow", "track": "allow", "recall": "allow",
"manual": "allow", "settings": "allow",
"team_status": "allow", "team_read": "allow", "team_send": "allow",
"team_stop": "allow", "team_post": "allow",
// A manager deciding or sending up a packet waiting on it, and bringing
// the person its closing report: each is a line in the team's own
// record, and none spends or starts anything.
"team_decide": "allow", "team_escalate": "allow", "team_close_report": "allow",
// A conflict raised to the manager above the parties: a packet and a
// line of Traffic, and nothing spent or started.
"team_raise": "allow",
}
}

Expand Down
15 changes: 15 additions & 0 deletions cmd/codeaf/chatv3_approvalfloor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,18 @@ func TestTheFloorDoesNotWidenAnySafetyFloor(t *testing.T) {
// And a bash call whose command cannot be read degrades rather than runs.
wantAction(t, policy, "bash", `{}`, approval.ActionPrompt)
}

// THE TEAM VERBS SPLIT READ AND MESSAGE FROM START (internal/session's
// tools_team.go). A manager looking at its team, sending it a line, ending a
// member's turn and a member posting to its room stay inside the team the person
// made and are logged in its traffic, so they sit on the floor; a new member is a
// new conversation that spends money, so team_start is the blanket mode's and
// asks under the shipped default.
func TestTheTeamVerbsAreAllowedAndTeamStartAsks(t *testing.T) {
dir := v3Profile(t, map[string]any{"tools.approvalMode": "prompt"})
policy := gateOf(t, dir)
for _, tool := range []string{"team_status", "team_read", "team_send", "team_stop", "team_post", "team_decide", "team_escalate", "team_close_report", "team_raise"} {
wantAction(t, policy, tool, `{"to":"web","text":"x","handle":"web"}`, approval.ActionAllow)
}
wantAction(t, policy, "team_start", `{"handle":"docs","brief":"write the README"}`, approval.ActionPrompt)
}
6 changes: 6 additions & 0 deletions cmd/codeaf/chatv3_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,12 @@ func hostOptions(fleet *engineFleet, welcome remote.Welcome, pick bool) (tui3.Op
// which is not an optimization but the seam's stated law, because that
// segment is asked on the frame. Save travels synchronously: it is a
// keystroke, it is rare, and somebody is waiting for its answer.
// THE TEAMS, AS THE ENGINE MACHINE KEEPS THEM. The far session's team
// tools write the teams file and each team's Traffic into the engine's
// profile, so the window reads and writes those, over the wire
// ([hostTeams]); an engine without the doors hands no seam and the
// surface turns teams off rather than keeping them on this laptop.
Teams: hostTeamsSeam(far, welcome),
Standing: tui3.StandingSeam{
Items: stands.list,
Save: stands.save,
Expand Down
277 changes: 277 additions & 0 deletions cmd/codeaf/chatv3_host_teams.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
package main

import (
"errors"
"sync"

"github.com/Agent-Field/codeaf/internal/remote"
teamstore "github.com/Agent-Field/codeaf/internal/teams"
"github.com/Agent-Field/codeaf/internal/tui3"
)

// hostTeams is [tui3.TeamsSeam] over the wire: the ENGINE machine's teams file
// and Traffic logs, which are where the far session's team tools keep them
// (internal/remote's wire_teams.go).
//
// IT IS BUILT LIKE [hostStanding], for the one call the surface makes on its
// loop. [tui3.TeamsSeam.Load] is asked at an opening and must not block, so it
// answers what is held and nothing else; when nothing is held yet it says so,
// and the surface asks [tui3.TeamsSeam.ReadSince] off its loop, once, which
// fills what is held. Every other door here is called from a command and may
// take a round trip: the Traffic clock's turn, and the write an edit queued.
// There is no clock of its own: the surface's Traffic clock is the only thing
// that asks again, and it runs only while a managed team is held.
//
// NOTHING HERE READS THIS LAPTOP'S PROFILE. The teams a window over --host
// draws are the far machine's, or none: an engine without the doors gets no
// seam at all ([hostTeamsSeam]), and the surface turns teams off rather than
// falling back to the file on this disk, which the far session never reads.
type hostTeams struct {
// read, write and traffic are the three wire doors, as closures for
// [hostStanding]'s reason: a test hands them a conflict without a pipe.
read func(stamp string, reserved []float64) (remote.TeamsReading, error)
write func(base string, teams []teamstore.Team) (remote.TeamsReading, error)
traffic func(team, after string, limit int) (remote.TeamsTraffic, error)

// The delegation doors, nil when the engine does not answer them
// ([remote.Welcome.Delegation]); the seam then hands the surface none.
defaults func() (teamstore.Defaults, error)
// applyDefault writes one `teams.` row ([remote.Welcome.TeamSettings]).
// Nil leaves the settings Teams tab read-only.
applyDefault func(key, raw string) (teamstore.Defaults, error)
packets func(scope, stamp string) (remote.PacketsReading, error)
raise func(p teamstore.Packet) (teamstore.Packet, error)
decide func(id, by, decision, reason string) (teamstore.Packet, error)
escalate func(id, by, to, reason string) (teamstore.Packet, error)
spend func(team, day, stamp string) (remote.SpendReading, error)
deleteOne func(team string) (remote.DeleteTeamReply, error)
// The wrap-up's two doors, nil when the engine does not answer them
// ([remote.Welcome.WrapUp]).
wrapUp func(team, text string) error
acceptClosing func(id string) (remote.AcceptClosingReply, error)

mu sync.Mutex
// teams and stamp are the last list the engine answered with and the
// file's stamp it was at; known says there has been one.
teams []teamstore.Team
stamp string
known bool
// reserved is the palette's reserved hues as the surface last passed
// them, for a read this seam makes on its own.
reserved []float64
}

// hostTeamsTries is how many times a write that met another writer is made
// again from a fresh read before it gives up and says so.
const hostTeamsTries = 3

// errHostTeamsBusy is a write that met another writer on every try.
var errHostTeamsBusy = errors.New("the teams on the far machine kept changing while this was written; try again")

func newHostTeams(far hostFar, delegation, wrapUp, settings bool) *hostTeams {
h := &hostTeams{
read: far.client.TeamsRead,
write: far.client.TeamsUpdate,
traffic: far.client.TeamsTraffic,
}
if delegation {
c := far.client
h.defaults, h.packets, h.raise = c.TeamsDefaults, c.TeamsPackets, c.TeamsRaise
h.decide, h.escalate, h.spend, h.deleteOne = c.TeamsDecide, c.TeamsEscalate, c.TeamsSpend, c.TeamsDelete
if wrapUp {
h.wrapUp, h.acceptClosing = c.TeamsWrapUp, c.TeamsAcceptClosing
}
}
if settings && far.client != nil {
h.applyDefault = far.client.TeamsApplyDefault
}
return h
}

// hostTeamsSeam is the seam the --host door hands the surface: the far
// machine's teams when the engine answers the teams doors, and the zero seam
// when it does not or there is no connection. The zero seam over --host is
// teams off (internal/tui3's [app.teamsOff]), never this laptop's file.
func hostTeamsSeam(far hostFar, welcome remote.Welcome) tui3.TeamsSeam {
if far.client == nil || !welcome.Teams {
return tui3.TeamsSeam{}
}
return newHostTeams(far, welcome.Delegation, welcome.WrapUp, welcome.TeamSettings).seam()
}

// seam is h as the surface's functions. The delegation doors are handed only
// when the engine answers them, so an older engine's window has a seam whose
// delegation doors are nil, which the surface says rather than guesses at.
func (h *hostTeams) seam() tui3.TeamsSeam {
s := tui3.TeamsSeam{Load: h.load, ReadSince: h.readSince, Update: h.update, Traffic: h.readTraffic}
if h.applyDefault != nil {
s.ApplyDefault = h.applyDefault
}
if h.defaults == nil {
return s
}
s.Defaults, s.Raise, s.Decide, s.Escalate = h.defaults, h.raise, h.decide, h.escalate
s.Packets, s.Spend, s.Delete = h.readPackets, h.readSpend, h.forget
if h.wrapUp != nil && h.acceptClosing != nil {
s.WrapUp, s.AcceptClosing = h.wrapUp, h.closeOnReport
}
return s
}

// closeOnReport is [tui3.TeamsSeam.AcceptClosing]: the engine closes the team
// on its report, and the list held here is read again, because the file moved.
func (h *hostTeams) closeOnReport(id string) (bool, error) {
reply, err := h.acceptClosing(id)
if err != nil {
return false, err
}
if reply.Closed {
if _, _, _, err := h.readSince("", h.reservedHues()); err != nil {
return true, err
}
}
return reply.Closed, nil
}

// readPackets is [tui3.TeamsSeam.Packets]: one round trip, a few bytes when
// the engine's packet files have not moved.
func (h *hostTeams) readPackets(scope, since string) ([]teamstore.Packet, string, bool, error) {
got, err := h.packets(scope, since)
if err != nil {
return nil, "", false, err
}
return got.Packets, got.Stamp, got.Same, nil
}

// readSpend is [tui3.TeamsSeam.Spend]: one round trip, a few bytes when
// neither the engine's teams file nor its ledger moved.
func (h *hostTeams) readSpend(team, day, since string) (teamstore.Spend, string, bool, error) {
got, err := h.spend(team, day, since)
if err != nil {
return teamstore.Spend{}, "", false, err
}
if got.Same {
return teamstore.Spend{}, got.Stamp, true, nil
}
if got.Spend == nil {
return teamstore.Spend{Team: team, Day: day}, got.Stamp, false, nil
}
return *got.Spend, got.Stamp, false, nil
}

// forget is [tui3.TeamsSeam.Delete]: the engine forgets the team and its
// files, and the list held here is read again, because the file moved.
func (h *hostTeams) forget(team string) ([]string, error) {
reply, err := h.deleteOne(team)
if err != nil {
return nil, err
}
if _, _, _, err := h.readSince("", h.reservedHues()); err != nil {
return reply.Gone, err
}
return reply.Gone, nil
}

// load is [tui3.TeamsSeam.Load]: what is held, now, and never the wire.
func (h *hostTeams) load(reserved []float64) ([]teamstore.Team, string, bool) {
return h.held(reserved)
}

// held is the list held, a copy, and remembers the reserved hues asked with.
func (h *hostTeams) held(reserved []float64) ([]teamstore.Team, string, bool) {
h.mu.Lock()
defer h.mu.Unlock()
if reserved != nil {
h.reserved = append([]float64(nil), reserved...)
}
return cloneTeams(h.teams), h.stamp, h.known
}

// keep holds a list the engine answered with.
func (h *hostTeams) keep(teams []teamstore.Team, stamp string) {
h.mu.Lock()
defer h.mu.Unlock()
h.teams, h.stamp, h.known = cloneTeams(teams), stamp, true
}

// readSince is [tui3.TeamsSeam.ReadSince]: one round trip, whose answer is a
// few bytes when the file is still at since.
func (h *hostTeams) readSince(since string, reserved []float64) ([]teamstore.Team, string, bool, error) {
reading, err := h.read(since, reserved)
if err != nil {
return nil, "", false, err
}
if reading.Same {
return nil, reading.Stamp, true, nil
}
h.keep(reading.Teams, reading.Stamp)
return reading.Teams, reading.Stamp, false, nil
}

// update is [tui3.TeamsSeam.Update] as a compare-and-swap over the wire.
//
// THE LOCK CANNOT CROSS THE WIRE, SO THE WRITE IS CONDITIONAL. The change is
// made to the list held (or read now, when none is), and the whole list goes
// back with the stamp it was made from; the engine writes it only if its file
// is still at that stamp (internal/teams' ChangeIf). A file that moved in
// between (the far session made a manager, set a handle, started a member)
// answers Stale, and the list is read again and the change made again on top
// of it, up to [hostTeamsTries] times. So what another writer did is kept, as
// the local store's read-modify-write keeps it, and nothing is ever written
// over a list this window did not see.
func (h *hostTeams) update(change func(*teamstore.File) error) ([]teamstore.Team, string, error) {
teams, stamp, known := h.held(nil)
for try := 0; try < hostTeamsTries; try++ {
if !known || try > 0 {
reserved := h.reservedHues()
fresh, at, _, err := h.readSince("", reserved)
if err != nil {
return nil, "", err
}
teams, stamp = fresh, at
}
f := &teamstore.File{Version: teamstore.Version, Teams: cloneTeams(teams)}
if err := change(f); err != nil {
return nil, "", err
}
reply, err := h.write(stamp, f.Teams)
if err != nil {
return nil, "", err
}
if reply.Stale {
continue
}
h.keep(reply.Teams, reply.Stamp)
return reply.Teams, reply.Stamp, nil
}
return nil, "", errHostTeamsBusy
}

// reservedHues is the palette's reserved hues as last passed.
func (h *hostTeams) reservedHues() []float64 {
h.mu.Lock()
defer h.mu.Unlock()
return append([]float64(nil), h.reserved...)
}

// readTraffic is [tui3.TeamsSeam.Traffic]: one round trip, answered from a
// stat on the engine when the log has not moved.
func (h *hostTeams) readTraffic(team, after string, limit int) ([]teamstore.Entry, error) {
got, err := h.traffic(team, after, limit)
if err != nil {
return nil, err
}
return got.Entries, nil
}

// cloneTeams is a copy of teams that shares nothing with it.
func cloneTeams(teams []teamstore.Team) []teamstore.Team {
if teams == nil {
return nil
}
out := make([]teamstore.Team, len(teams))
for i, t := range teams {
out[i] = t.Clone()
}
return out
}
Loading