Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/changes/unreleased/1797-default-team-tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
kind: changed
title: Team chat sidebars default to Tasks
pr: 1797
surface: [chat, docs]
invalidates:
- "Manager conversations in Chats opened the sidebar on Traffic. Team overlays now default to Tasks for managers and members, while retaining explicit selections and offering both Tasks and Traffic."
---

The sidebar keeps its width and header position when switching views. The session
continues to remember manager and member selections independently, including when
a team overlay is cleared and reopened.
2 changes: 1 addition & 1 deletion internal/manual/chat/screen.md
Original file line number Diff line number Diff line change
Expand Up @@ -3305,7 +3305,7 @@ ctrl+. earlier
in front is bold ink and the other dim; each is a button with a hover ground and a hint,
and the other word keeps its count, so what arrives on the Traffic while you read the
tasks still says `2 new`. Which word is in front is remembered for the session per kind
of chat: a manager's chat opens on the Traffic, every other chat on the Tasks. The `alt+l`
of chat: every chat starts on Tasks, including team overlays, and Traffic stays selectable. The `alt+l`
at the right closes the column.
- **The needs-you band** is everything that needs you now, from both views: a task whose
next step is yours, a member's question to you, a decision put to you (each led by `?` in
Expand Down
8 changes: 4 additions & 4 deletions internal/manual/chat/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -1986,10 +1986,10 @@ click`); press the other word to bring it to the front. A count of `0` is dim. A
of the header is the column's own key, `alt+l` (`opt+l` on a Mac), and it is a button too:
press it and the column goes away (*Hiding the task column* below).

**Which word is in front is remembered for the session, per kind of chat.** A manager's
chat opens on the Traffic and every other chat opens on the Tasks. Change it in a member's
chat and every member's chat you visit this session opens the same way; the manager's chat
keeps its own answer. The team manager page has the Traffic view.
**Which word is in front is remembered for the session, per kind of chat.** Every chat
opens on the Tasks, including the manager and members in a team overlay. Change it in a
member's chat and every member's chat you visit this session opens the same way; the
manager's chat keeps its own answer. Tasks and Traffic remain selectable. The team manager page has the Traffic view.

**Under the header is the needs-you band**, when anything needs you, and nothing at all
when nothing does:
Expand Down
4 changes: 2 additions & 2 deletions internal/manual/chat/team-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ On the right is the conversation's column, the same one every chat has. Its head
words, `Tasks 14 · Traffic 8` with `alt+l` at its right: the word in front is in bold ink and
the other is dim, each is a button (point at it for what it shows, press it to bring it to
the front), and while the tasks are in front the Traffic word keeps counting what arrived,
`Traffic 3 new`. With the manager in front the column opens on the **Traffic**; with a member
in front it opens on the tasks. Each kind of chat remembers the word you last chose for the
`Traffic 3 new`. With a team overlay active in Chats, the column opens on **Tasks** for
both the manager and members; **Traffic** remains selectable. Each kind of chat remembers the word you last chose for the
rest of the session. With the column holding the keyboard (`alt+t`), `←` and `→` switch the
words. Nothing here moves the conversation: both words are drawn in the same columns.

Expand Down
95 changes: 51 additions & 44 deletions internal/tui3/managercolumn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
// remembered open.
func managerColumnApp(t *testing.T) *app {
t.Helper()
a, _, _, _ := trafficApp(t)
a, _, _, _ := teamChatApp(t)
a.width, a.height = 160, 40
a.railAway = false
a.welcome.open = false
Expand Down Expand Up @@ -52,51 +52,47 @@ func sideWordAt(t *testing.T, a *app, view int) (int, int) {
return 0, 0
}

// WITH THE MANAGER IN FRONT THE ONE COLUMN OPENS ON THE TRAFFIC, and its
// header offers the manager's own tasks as the other word, with its count,
// even at zero. A press on the word lays the tasks in the same column at the
// same width with the header where it was, and a press on the Traffic word
// takes it back. There is no second column and no special case.
func TestManagerColumnOpensOnTheTrafficAndOffersItsTasks(t *testing.T) {
// A TEAM OVERLAY OPENS ON TASKS, with Traffic still selectable. Switching
// either word leaves the column, header and conversation in place.
func TestManagerColumnOpensOnTasksAndOffersTraffic(t *testing.T) {
a := managerColumnApp(t)
if _, ok := a.teamActive(); !ok || a.teamViews.id == "" {
t.Fatal("the fixture has no active team overlay")
}
rows := railLines(t, a)
head := railRowOf(rows, sideTasksWord+" 0"+sideWordSep+sideTrafficWord)
if head < 0 || a.sideView() != sideTraffic {
t.Fatalf("the manager's column does not open on the Traffic with both words:\n%s", strings.Join(rows, "\n"))
if head < 0 || a.sideView() != sideTasks {
t.Fatalf("the team overlay does not open on Tasks with both words:\n%s", strings.Join(rows, "\n"))
}
managerTasks(a, 2)
rows = railLines(t, a)
if railRowOf(rows, sideTasksWord+" 2"+sideWordSep) != head || railRowOf(rows, "Task 1") >= 0 {
t.Fatalf("the Tasks word does not count the work behind it:\n%s", strings.Join(rows, "\n"))
if railRowOf(rows, sideTasksWord+" 2"+sideWordSep) != head || railRowOf(rows, "Task 1") <= head {
t.Fatalf("the default Tasks view does not show the manager's work:\n%s", strings.Join(rows, "\n"))
}
body, cols := a.bodyWidth(), a.railWidth()

x, y := sideWordAt(t, a, sideTasks)
a.setHover(x, y)
if a.hot.kind != hoverSide || a.hot.key != sideHeadKey || a.hot.index < 0 {
t.Fatalf("the Tasks word does not answer the pointer: %+v", a.hot)
}
if words := a.dockHoverWords(); !strings.Contains(words, "tasks") || !strings.Contains(words, "click") {
t.Fatalf("the Tasks word's hint says %q", words)
}
a.dropHover()
sideClick(t, a, x, y)
if a.sideView() != sideTasks {
t.Fatal("a press on Tasks did not bring the tasks to the front")
}
rows = railLines(t, a)
if a.bodyWidth() != body || a.railWidth() != cols || railRowOf(rows, sideTasksWord+" 2") != head || railRowOf(rows, "Task 1") <= head {
t.Fatalf("the tasks are not in the same column under the same header (body %d->%d, cols %d->%d):\n%s", body, a.bodyWidth(), cols, a.railWidth(), strings.Join(rows, "\n"))
}
x, y = sideWordAt(t, a, sideTraffic)
sideClick(t, a, x, y)
if a.sideView() != sideTraffic || a.bodyWidth() != body {
t.Fatal("a press on Traffic did not take the column back, or moved the body")
for _, view := range []int{sideTraffic, sideTasks} {
x, y := sideWordAt(t, a, view)
a.setHover(x, y)
if a.hot.kind != hoverSide || a.hot.key != sideHeadKey || a.hot.index < 0 {
t.Fatalf("view %d does not answer the pointer: %+v", view, a.hot)
}
if words := a.dockHoverWords(); !strings.Contains(words, "click") {
t.Fatalf("view %d has no click hint: %q", view, words)
}
a.dropHover()
sideClick(t, a, x, y)
rows = railLines(t, a)
if a.sideView() != view || a.bodyWidth() != body || a.railWidth() != cols || railRowOf(rows, sideTasksWord+" 2") != head {
t.Fatalf("view %d did not switch in the same column:\n%s", view, strings.Join(rows, "\n"))
}
if shown := railRowOf(rows, "Task 1") > head; shown != (view == sideTasks) {
t.Fatalf("view %d shows tasks=%v:\n%s", view, shown, strings.Join(rows, "\n"))
}
}
}

// THE WORD IN FRONT IS REMEMBERED PER KIND OF CHAT, for the session: a
// manager opens on the Traffic, a member and a chat in no team on the tasks,
// manager, a member and a chat in no team open on the tasks,
// and a word the person chose in one kind of chat is what that kind opens on
// next, whatever the other kind was left on. A chat in no team has only the
// one word.
Expand All @@ -105,26 +101,37 @@ func TestTheColumnsWordIsRememberedPerKindOfChat(t *testing.T) {
harbor := a.wall.teams[0].ID
manager := a.frontTabKey()
_, priceKey := trafficHandle(t, a, harbor, "openrouter")
if a.sideView() != sideTraffic {
t.Fatal("a manager does not open on the Traffic")
if a.sideView() != sideTasks {
t.Fatal("a manager does not open on Tasks")
}
a.sideSetView(sideTasks)
a.sideSetView(sideTraffic)

spend(t, a, a.trafficGo(priceKey))
if a.sideKind() != sideKindMember || a.sideView() != sideTasks {
t.Fatalf("a member opens on view %d", a.sideView())
}
a.sideSetView(sideTraffic)
a.sideSetView(sideTasks)

spend(t, a, a.trafficGo(manager))
if a.sideKind() != sideKindManager || a.sideView() != sideTasks {
if a.sideKind() != sideKindManager || a.sideView() != sideTraffic {
t.Fatalf("the manager forgot its word: kind %d view %d", a.sideKind(), a.sideView())
}
spend(t, a, a.trafficGo(priceKey))
if a.sideView() != sideTraffic {
if a.sideView() != sideTasks {
t.Fatal("the member forgot its word")
}

spend(t, a, a.trafficGo(manager))
a.teamViewSet("")
if a.sideKind() != sideKindPlain || a.sideView() != sideTasks {
t.Fatal("clearing the overlay did not restore ordinary Tasks")
}
spend(t, a, a.teamActivate(harbor))
if a.sideView() != sideTraffic {
t.Fatal("reactivating the overlay forgot the chosen Traffic view")
}

plainChat, _, _ := tabApp(t)
plainChat.profileDir = t.TempDir()
plainChat.width, plainChat.height = 160, 40
Expand All @@ -148,20 +155,20 @@ func TestLeftAndRightSwitchTheColumnsWords(t *testing.T) {
managerTasks(a, 2)
front := a.frontTabKey()
drive(t, a, key("right"))
if a.sideView() != sideTraffic {
if a.sideView() != sideTasks {
t.Fatal("an arrow with the keyboard in the draft switched the column")
}
drive(t, a, altT())
if !a.railHold {
t.Fatal("alt+t did not give the column the keyboard")
}
drive(t, a, key("right"))
if a.sideView() != sideTasks || !a.railHold || a.frontTabKey() != front {
t.Fatalf("right did not switch to the tasks in place: view %d hold %v", a.sideView(), a.railHold)
if a.sideView() != sideTraffic || !a.railHold || a.frontTabKey() != front {
t.Fatalf("right did not switch to Traffic in place: view %d hold %v", a.sideView(), a.railHold)
}
drive(t, a, key("left"))
if a.sideView() != sideTraffic || !a.railHold {
t.Fatal("left did not switch back to the Traffic")
if a.sideView() != sideTasks || !a.railHold {
t.Fatal("left did not switch back to Tasks")
}
drive(t, a, key("esc"))
if a.railHold {
Expand Down
10 changes: 3 additions & 7 deletions internal/tui3/sidecol.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ import (
// hover ground and a hint. The other word keeps its count, so what arrived in
// the Traffic while the tasks are in front is still on screen (`Traffic 3
// new`). Which word is in front is remembered for the session per kind of chat
// ([sideState.view]): a manager chat opens on the Traffic, every other chat on
// the tasks.
// ([sideState.view]): every chat opens on the tasks until the person chooses
// otherwise.
//
// THE BAND IS WHAT NEEDS THE PERSON NOW, from both sources ([app.sideBand]):
// a question or a packet waiting on them, a task whose next step is theirs,
Expand Down Expand Up @@ -240,8 +240,7 @@ func (a *app) sideKind() int {
}

// sideView is the word in front: the person's own choice for this kind of
// chat, else the Traffic in a manager chat and the tasks everywhere else. A
// chat in no team has only the tasks.
// chat, else the tasks. A chat in no team has only the tasks.
func (a *app) sideView() int {
kind := a.sideKind()
if kind == sideKindPlain {
Expand All @@ -250,9 +249,6 @@ func (a *app) sideView() int {
if v := a.side.view[kind]; v != 0 {
return v
}
if kind == sideKindManager {
return sideTraffic
}
return sideTasks
}

Expand Down
1 change: 1 addition & 0 deletions internal/tui3/teamseam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ func TestTeamsOverAHostedSeamRefuseNothingAndLeaveTheLaptopFileAlone(t *testing.
// nothing on the laptop.
a.width, a.height = 160, 40
a.welcome.open = false
a.sideSetView(sideTraffic)
if col := strings.Join(railLines(t, a), "\n"); !strings.Contains(col, "take the lexer") || a.sideView() != sideTraffic {
t.Fatalf("the column over --host does not draw the engine's Traffic:\n%s", col)
}
Expand Down
17 changes: 13 additions & 4 deletions internal/tui3/teamtraffic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import (
teamstore "github.com/Agent-Field/codeaf/internal/teams"
)

// trafficApp is the strip's three conversations as one team, harbor, on a
// teamChatApp is the strip's three conversations as one team, harbor, on a
// profile of the test's own, with the conversation in front its manager and
// the two behind it its members. older and newer are the agents behind.
func trafficApp(t *testing.T) (a *app, harbor string, older, newer *fakeAgent) {
func teamChatApp(t *testing.T) (a *app, harbor string, older, newer *fakeAgent) {
t.Helper()
a, older, newer = tabApp(t)
a.profileDir = t.TempDir()
Expand All @@ -41,6 +41,15 @@ func trafficApp(t *testing.T) (a *app, harbor string, older, newer *fakeAgent) {
return a, harbor, older, newer
}

// trafficApp selects Traffic for tests of its rows and controls, independently
// of the initial view a team chat offers.
func trafficApp(t *testing.T) (a *app, harbor string, older, newer *fakeAgent) {
t.Helper()
a, harbor, older, newer = teamChatApp(t)
a.sideSetView(sideTraffic)
return a, harbor, older, newer
}

// trafficHandle is the handle of the member whose tab says word.
func trafficHandle(t *testing.T, a *app, id, word string) (string, string) {
t.Helper()
Expand Down Expand Up @@ -75,7 +84,7 @@ func trafficReadNow(t *testing.T, a *app) {
teamsFlush(t, a)
}

// THE COLUMN IS THE CACHE, BESIDE THE MANAGER, AND IT OPENS ON THE TRAFFIC.
// THE TRAFFIC COLUMN IS THE CACHE, BESIDE THE MANAGER.
// With the manager in front on a wide frame the right of the body is the side
// column, its header `Tasks 0 · Traffic N` with the Traffic in front; the
// directive is a row of work, the note that answers nothing is General's; the
Expand All @@ -98,7 +107,7 @@ func TestTrafficColumnBesideTheManager(t *testing.T) {
t.Fatalf("the cache holds %d entries", got)
}
if a.sideView() != sideTraffic {
t.Fatal("a manager's column does not open on the Traffic")
t.Fatal("the selected Traffic view is not in front")
}
a.sideToggleThread(sideThreadKey(harbor, sideGeneral))
rows := railLines(t, a)
Expand Down
Loading