From 39d0c877e9f1f52ec88b0eb0d1226e4f6a2752fa Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Mon, 26 Oct 2026 00:28:21 -0400 Subject: [PATCH 1/3] fix(tui): keep cold team tab opens off the input loop --- internal/manual/chat/sessions-and-rewind.md | 8 +++++++ internal/tui3/hop.go | 23 +++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/internal/manual/chat/sessions-and-rewind.md b/internal/manual/chat/sessions-and-rewind.md index 879118479d..1b3770385f 100644 --- a/internal/manual/chat/sessions-and-rewind.md +++ b/internal/manual/chat/sessions-and-rewind.md @@ -977,3 +977,11 @@ with the error, so your message is not lost or sent to the old chat. Returning to an already open chat keeps its last displayed text visible while its current history refreshes. The refreshed history replaces that preview. + +## Switching to a team chat that is not open yet + +A team tab can name a manager or worker that this window has not opened yet. +In windows that can keep several chats open, its connection opens in the +background, with an opening notice on the current chat. You can select another tab or cancel with Escape while the engine is +busy. A canceled opening does not replace the selected chat when it finishes. +Chats already open in this window switch directly. diff --git a/internal/tui3/hop.go b/internal/tui3/hop.go index 87ade6d932..e1728c6a4a 100644 --- a/internal/tui3/hop.go +++ b/internal/tui3/hop.go @@ -1,6 +1,7 @@ package tui3 import ( + "errors" "path/filepath" "sort" "strings" @@ -1222,6 +1223,28 @@ func (a *app) hopStart(row hopRow) tea.Cmd { a.hopSay(WorkspaceGoneWord + " · " + row.where) return nil } + if !a.shared && a.open != nil { + if cmd, ours := a.bringForward(row.file); ours { + return a.hopLand(cmd) + } + // A team's visible tab may not have a connection in this window yet. + // Opening it can wait on the engine; keep later clicks and typing live. + // Shared handles retain their existing swap contract because cancelling + // a swap cannot safely release the returned handle independently. + open := a.open + return a.conversationLater(func() (Conversation, error) { + return open(row.where, row.file) + }, a.hopSay, func(conv Conversation) tea.Cmd { + return a.hopLand(a.takeBeside(conv)) + }, func(err error) tea.Cmd { + if errors.Is(err, session.ErrSessionLocked) { + a.hopSay(sessionBusyWord) + } else { + a.hopSay(err.Error()) + } + return nil + }) + } cmd, refusal := a.openBeside(row.where, row.file) if refusal != "" { a.hopSay(refusal) From 18265d701a24f359d6090199d9b6e5daae8892c4 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Mon, 26 Oct 2026 00:29:16 -0400 Subject: [PATCH 2/3] docs: record responsive cold team tab opening --- .../unreleased/1951-cold-team-tab-navigation.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 docs/changes/unreleased/1951-cold-team-tab-navigation.md diff --git a/docs/changes/unreleased/1951-cold-team-tab-navigation.md b/docs/changes/unreleased/1951-cold-team-tab-navigation.md new file mode 100644 index 0000000000..e68f3e5563 --- /dev/null +++ b/docs/changes/unreleased/1951-cold-team-tab-navigation.md @@ -0,0 +1,14 @@ +--- +kind: fixed +title: Keep team navigation responsive while opening a worker for the first time +pr: 1951 +surface: [chat, docs] +invalidates: + - "Opening an unseen team chat previously blocked input; independent chat opens now run in the background." +--- + +The manager and worker tabs remain usable while a first connection waits on the +engine. Canceling or selecting another chat prevents a late opening from taking +over the selected conversation. Already-open chats keep their immediate switch. +Connections limited to a single shared conversation retain their existing swap +behavior. From 7ac3c41b1f163bae066d1c55dcd15c8b9d7e6767 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Mon, 26 Oct 2026 00:33:27 -0400 Subject: [PATCH 3/3] fix(tui): settle team selection after a cold tab opens --- internal/tui3/chattabs.go | 12 ++++++++++-- internal/tui3/conversation_open.go | 3 +++ internal/tui3/hop.go | 2 ++ internal/tui3/teamoverlay.go | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/internal/tui3/chattabs.go b/internal/tui3/chattabs.go index 918c607111..42b42f592a 100644 --- a/internal/tui3/chattabs.go +++ b/internal/tui3/chattabs.go @@ -1424,8 +1424,8 @@ func (a *app) tabPress(x, y int) (tea.Cmd, bool) { // "that folder is gone" is two features to keep in step. func (a *app) tabGo(tab chatTab) (cmd tea.Cmd) { defer func() { - if t, ok := a.teamActive(); ok && !a.wall.on && !a.teamOverlayHolds(t, a.frontTabKey()) { - a.teamViewSet("") + if !a.conversationOpening { + a.tabSettleTeam() } }() if tab.work { @@ -1456,6 +1456,14 @@ func (a *app) tabGo(tab chatTab) (cmd tea.Cmd) { return a.hopStart(hopRow{file: tab.file, where: tab.where, title: tab.word}) } +// tabSettleTeam validates the overlay against the conversation actually selected, +// after an immediate switch or after an asynchronous opening has settled. +func (a *app) tabSettleTeam() { + if t, ok := a.teamActive(); ok && !a.wall.on && !a.teamOverlayHolds(t, a.frontTabKey()) { + a.teamViewSet("") + } +} + // ── DISMISSING A TAB ──────────────────────────────────────────────────────── // tabDismiss removes a conversation's tab from this window. Its transcript and diff --git a/internal/tui3/conversation_open.go b/internal/tui3/conversation_open.go index cdd06fcd0c..3e46c2ed63 100644 --- a/internal/tui3/conversation_open.go +++ b/internal/tui3/conversation_open.go @@ -152,6 +152,9 @@ func (a *app) conversationLater(ask func() (Conversation, error), say func(strin } func (a *app) cancelConversationOpening() { + if a.conversationPreview == nil { + defer a.tabSettleTeam() + } a.restoreOpeningDraft(a.conversationPreview) a.conversationOpening = false a.conversationPreview = nil diff --git a/internal/tui3/hop.go b/internal/tui3/hop.go index e1728c6a4a..cca21833d1 100644 --- a/internal/tui3/hop.go +++ b/internal/tui3/hop.go @@ -1235,8 +1235,10 @@ func (a *app) hopStart(row hopRow) tea.Cmd { return a.conversationLater(func() (Conversation, error) { return open(row.where, row.file) }, a.hopSay, func(conv Conversation) tea.Cmd { + defer a.tabSettleTeam() return a.hopLand(a.takeBeside(conv)) }, func(err error) tea.Cmd { + defer a.tabSettleTeam() if errors.Is(err, session.ErrSessionLocked) { a.hopSay(sessionBusyWord) } else { diff --git a/internal/tui3/teamoverlay.go b/internal/tui3/teamoverlay.go index 5c66cdfd6b..dd6689a0c2 100644 --- a/internal/tui3/teamoverlay.go +++ b/internal/tui3/teamoverlay.go @@ -93,7 +93,7 @@ func (a *app) teamOverlaySync() tea.Cmd { } a.teamViews.pending = target.key cmd := a.tabGo(target) - if a.frontTabKey() != target.key { + if !a.conversationOpening && a.frontTabKey() != target.key { a.teamViewSet("") } return cmd