From 66d6718773725b23b18921bc79c34ef9792d09c7 Mon Sep 17 00:00:00 2001 From: ZeroPoint95 <329227198+ZeroPoint95@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:04:02 -0400 Subject: [PATCH 1/2] Default team chat sidebar to Tasks --- internal/manual/chat/screen.md | 2 +- internal/manual/chat/tasks.md | 8 +-- internal/manual/chat/team-manager.md | 4 +- internal/tui3/managercolumn_test.go | 95 +++++++++++++++------------- internal/tui3/sidecol.go | 10 +-- internal/tui3/teamseam_test.go | 1 + internal/tui3/teamtraffic_test.go | 17 +++-- 7 files changed, 75 insertions(+), 62 deletions(-) diff --git a/internal/manual/chat/screen.md b/internal/manual/chat/screen.md index 311a05d040..20ba26c635 100644 --- a/internal/manual/chat/screen.md +++ b/internal/manual/chat/screen.md @@ -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 diff --git a/internal/manual/chat/tasks.md b/internal/manual/chat/tasks.md index 53f74101b9..fab5edfc22 100644 --- a/internal/manual/chat/tasks.md +++ b/internal/manual/chat/tasks.md @@ -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: diff --git a/internal/manual/chat/team-manager.md b/internal/manual/chat/team-manager.md index e7c3bd341a..060f58eabf 100644 --- a/internal/manual/chat/team-manager.md +++ b/internal/manual/chat/team-manager.md @@ -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. diff --git a/internal/tui3/managercolumn_test.go b/internal/tui3/managercolumn_test.go index 949a531fcc..f85ab5406e 100644 --- a/internal/tui3/managercolumn_test.go +++ b/internal/tui3/managercolumn_test.go @@ -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 @@ -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. @@ -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 @@ -148,7 +155,7 @@ 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()) @@ -156,12 +163,12 @@ func TestLeftAndRightSwitchTheColumnsWords(t *testing.T) { 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 { diff --git a/internal/tui3/sidecol.go b/internal/tui3/sidecol.go index c497c59422..bdd157e502 100644 --- a/internal/tui3/sidecol.go +++ b/internal/tui3/sidecol.go @@ -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, @@ -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 { @@ -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 } diff --git a/internal/tui3/teamseam_test.go b/internal/tui3/teamseam_test.go index c1b3f43856..73588ebf9d 100644 --- a/internal/tui3/teamseam_test.go +++ b/internal/tui3/teamseam_test.go @@ -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) } diff --git a/internal/tui3/teamtraffic_test.go b/internal/tui3/teamtraffic_test.go index 4a1fadbd42..568188d7b1 100644 --- a/internal/tui3/teamtraffic_test.go +++ b/internal/tui3/teamtraffic_test.go @@ -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() @@ -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() @@ -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 @@ -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) From ec2b69adf703bd3e0e20dd9a41fe500d8d2f96c5 Mon Sep 17 00:00:00 2001 From: ZeroPoint95 <329227198+ZeroPoint95@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:41:51 -0400 Subject: [PATCH 2/2] Document team sidebar default change --- docs/changes/unreleased/1797-default-team-tasks.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 docs/changes/unreleased/1797-default-team-tasks.md diff --git a/docs/changes/unreleased/1797-default-team-tasks.md b/docs/changes/unreleased/1797-default-team-tasks.md new file mode 100644 index 0000000000..6dd2f7d5b2 --- /dev/null +++ b/docs/changes/unreleased/1797-default-team-tasks.md @@ -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.