fix(bot-mode): group chat opens as one room pane, not two (#89788)

Opening a Bot Mode group chat painted the room twice — once as a main-window
workspace tab (host.openWorkspace) and once as the in-panel fallback, because
the Bots pane rendered off $groupChatWorkspace alone. Two live panes with
independent drafts drove one shared engine, and the roster disappeared behind
the duplicate.

The in-panel room is the fallback surface, not a second copy: it now renders
only while no main tab owns the group. The selection atom stays set either way
so the roster row still highlights, and desktops without the door — or whose
door throws — keep the in-pane room.

Consolidates #89881, #90274 and #90398, which fixed the same bug.

Closes #89788

Co-authored-by: helix4u <helix4u@users.noreply.github.com>
This commit is contained in:
Teknium
2026-08-19 21:12:25 -07:00
committed by GitHub
parent 145cd763ca
commit 6851841112
2 changed files with 33 additions and 2 deletions
+11 -1
View File
@@ -9448,6 +9448,16 @@ function GroupChatWorkspace({ group, members, onBack }) {
* disband (or the room view's own Back) can retire the tab it opened. */
const groupChatMainTabs = new Map()
/** The in-panel room is the FALLBACK surface, not a second copy: it renders
* only while no main-window tab owns the group. On desktops with the door
* the room already lives in a main tab, and painting it here too produced
* two live panes with independent drafts driving one shared engine (#89788).
* The selection atom stays set either way so the roster row still
* highlights. */
function shouldRenderGroupChatInPane(group) {
return Boolean(group && !groupChatMainTabs.has(group))
}
function closeGroupChatMainTab(group) {
const close = groupChatMainTabs.get(group)
@@ -9737,7 +9747,7 @@ function BotsPane() {
const groupChatMembers = groupChatName ? groupChatMemberBots(groupChatName, roster, allMeta) : []
if (groupChatName && groupChatMembers.length) {
if (shouldRenderGroupChatInPane(groupChatName) && groupChatMembers.length) {
return jsx(GroupChatWorkspace, { group: groupChatName, members: groupChatMembers })
}
@@ -104,7 +104,7 @@ function load(turnScript, { busyUntilResumeCall } = {}) {
.replace(/^import .* from 'react\/jsx-runtime'\r?\n/m, '')
.replace('export default {', 'globalThis.plugin = {')
.concat(
'\nglobalThis.__gc = { sendToGroupChat, runGroupChatRounds, harvestStrandedGroupReply, resolveGroupResponders, parseGroupChatMentions, rotateGroupSpeakers, isGroupPassText, formatGroupChatLine, buildGroupChatTurnPrompt, trimGroupChatLog, disbandGroupChat, updateGroupChat, openGroupChat, closeGroupChatMainTab, $groupChats, $groupNeedsYou, $groupChatWorkspace, $botMeta, GROUP_CHAT_MAX_ROUNDS, GROUP_CHAT_MAX_MESSAGES };\n'
'\nglobalThis.__gc = { sendToGroupChat, runGroupChatRounds, harvestStrandedGroupReply, resolveGroupResponders, parseGroupChatMentions, rotateGroupSpeakers, isGroupPassText, formatGroupChatLine, buildGroupChatTurnPrompt, trimGroupChatLog, disbandGroupChat, updateGroupChat, openGroupChat, closeGroupChatMainTab, shouldRenderGroupChatInPane, $groupChats, $groupNeedsYou, $groupChatWorkspace, $botMeta, GROUP_CHAT_MAX_ROUNDS, GROUP_CHAT_MAX_MESSAGES };\n'
)
vm.runInNewContext(source, context, { filename: 'plugin.js' })
const storageWrites = new Map()
@@ -366,9 +366,30 @@ test('group selection follows main-window open and close', () => {
gc.openGroupChat('Core')
assert.equal(gc.$groupChatWorkspace.get(), 'Core')
// #89788: the main tab owns the room, so the pane keeps its roster.
assert.equal(gc.shouldRenderGroupChatInPane('Core'), false)
onClose()
assert.equal(gc.$groupChatWorkspace.get(), null)
assert.equal(gc.shouldRenderGroupChatInPane('Core'), true)
})
test('older and failed workspace hosts keep the in-pane group fallback', () => {
const older = load(() => '(pass)')
older.openGroupChat('Core')
assert.equal(older.$groupChatWorkspace.get(), 'Core')
assert.equal(older.shouldRenderGroupChatInPane('Core'), true)
const failed = load(() => '(pass)')
failed.host.openWorkspace = () => {
throw new Error('workspace unavailable')
}
failed.openGroupChat('Ops')
assert.equal(failed.$groupChatWorkspace.get(), 'Ops')
assert.equal(failed.shouldRenderGroupChatInPane('Ops'), true)
})
test('closing an older selected group does not clear the newer selection', () => {