fix(bot-mode): highlight selected group chat

Signed-off-by: Shawn Wang <32839114+enwaiax@users.noreply.github.com>
This commit is contained in:
Shawn Wang
2026-08-18 16:04:06 +08:00
committed by Teknium
parent 3809f740c6
commit 80f080b6ed
3 changed files with 60 additions and 7 deletions
+23 -5
View File
@@ -4659,13 +4659,16 @@ function activeBots(roster, activeProfile, gatewayState, now = Date.now()) {
function BotRow({ bot, onDelete, onEdit, onGroup }) {
const activeProfile = useValue(host.state.profile)
const focusedProfile = useValue($focusedBotProfile)
const activeGroup = useValue($groupChatWorkspace)
const meta = botRosterMeta(bot, useValue($botMeta))
const groups = botGroups(meta)
const last = bot.last_session
// Highlight follows the chat on screen (focused session's owner), not the
// gateway socket's home — a focused tab doesn't swap the socket, and on the
// old keying the wrong bot stayed highlighted while you read another's chat.
const isActive = !bot.remoteSource && bot.name === focusedProfile
// A selected group chat suppresses every bot-row highlight: the group row
// owns the selection then (#88979).
const isActive = !activeGroup && !bot.remoteSource && bot.name === focusedProfile
// Turn-busy is a SOCKET fact: only the gateway-home profile can be mid-turn.
const isGatewayHome = !bot.remoteSource && bot.name === activeProfile
const { shape, color, image } = botAppearance(bot.name, meta)
@@ -4722,6 +4725,7 @@ function BotRow({ bot, onDelete, onEdit, onGroup }) {
const open = async () => {
const generation = ++botOpenGeneration
haptic('tap')
$groupChatWorkspace.set(null)
$selectedBot.set(bot.name)
if (bot.remoteSource) {
@@ -9137,6 +9141,10 @@ function closeGroupChatMainTab(group) {
groupChatMainTabs.delete(group)
if ($groupChatWorkspace.get() === group) {
$groupChatWorkspace.set(null)
}
if (typeof close === 'function') {
try {
close()
@@ -9164,6 +9172,7 @@ function GroupChatMainView({ group }) {
* view on desktops whose SDK predates the main-area door. */
function openGroupChat(group) {
$groupNeedsYou.set({ ...$groupNeedsYou.get(), [group]: false })
$groupChatWorkspace.set(group)
if (typeof host.openWorkspace === 'function') {
try {
@@ -9171,7 +9180,13 @@ function openGroupChat(group) {
title: group,
minWidth: '24rem',
render: () => jsx(GroupChatMainView, { group }),
onClose: () => groupChatMainTabs.delete(group)
onClose: () => {
groupChatMainTabs.delete(group)
if ($groupChatWorkspace.get() === group) {
$groupChatWorkspace.set(null)
}
}
})
groupChatMainTabs.set(group, close)
@@ -9182,7 +9197,8 @@ function openGroupChat(group) {
}
}
$groupChatWorkspace.set(group)
// The selected-group atom was set before trying the main-window door, so
// older desktops naturally render the in-panel room as the fallback.
}
/** One group chat as ONE roster row — the Discord shape: stacked member
@@ -9190,7 +9206,7 @@ function openGroupChat(group) {
* (markdown flattened), relative time of the last activity, and the
* needs-you badge on the row itself. Sorts into the same recency ordering
* as bot rows; clicking opens the room in the main chat window. */
function GroupRow({ group, members, needsYou, onOpen }) {
function GroupRow({ active, group, members, needsYou, onOpen }) {
const rooms = useValue($groupChats)
const allMeta = useValue($botMeta)
const room = rooms[group] || { log: [] }
@@ -9214,7 +9230,8 @@ function GroupRow({ group, members, needsYou, onOpen }) {
},
className: cn(
'flex w-full min-w-0 max-w-full items-center gap-2.5 overflow-hidden rounded-md px-2 py-2 text-left transition-colors',
'hover:bg-(--chrome-action-hover)'
'hover:bg-(--chrome-action-hover)',
active && 'bg-(--ui-row-active-background)'
),
children: [
// Room picture when the user set one; else a composite avatar of up to
@@ -9639,6 +9656,7 @@ function BotsPane() {
? jsx(
GroupRow,
{
active: groupChatName === row.name,
group: row.name,
members: row.members,
needsYou: Boolean(groupNeedsYou[row.name]),
@@ -93,7 +93,7 @@ function load(turnScript) {
.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, $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, $groupChats, $groupNeedsYou, $groupChatWorkspace, $botMeta, GROUP_CHAT_MAX_ROUNDS, GROUP_CHAT_MAX_MESSAGES };\n'
)
vm.runInNewContext(source, context, { filename: 'plugin.js' })
const storageWrites = new Map()
@@ -101,7 +101,7 @@ function load(turnScript) {
storage: { get: () => null, set: (key, value) => storageWrites.set(key, value) },
register: () => undefined
})
return { ...context.__gc, calls, sessions, storageWrites }
return { ...context.__gc, calls, host: context.host, sessions, storageWrites }
}
const MEMBERS = [{ name: 'research', title: '' }, { name: 'builder', title: '' }, { name: 'ops', title: 'The Ops' }]
@@ -344,6 +344,39 @@ test('source contract: workspace + main-window door + prompt rules are wired', (
assert.match(pluginSource, /\[Group chat: "\$\{groupName\}"\]/)
})
test('group selection follows main-window open and close', () => {
const gc = load(() => '(pass)')
let onClose
gc.host.openWorkspace = (_id, options) => {
onClose = options.onClose
return () => onClose()
}
gc.openGroupChat('Core')
assert.equal(gc.$groupChatWorkspace.get(), 'Core')
onClose()
assert.equal(gc.$groupChatWorkspace.get(), null)
})
test('closing an older selected group does not clear the newer selection', () => {
const gc = load(() => '(pass)')
gc.host.openWorkspace = () => () => undefined
gc.openGroupChat('Core')
gc.openGroupChat('Ops')
gc.closeGroupChatMainTab('Core')
assert.equal(gc.$groupChatWorkspace.get(), 'Ops')
})
test('source contract: active group styling suppresses bot styling', () => {
assert.match(pluginSource, /const isActive = !activeGroup && !bot\.remoteSource && bot\.name === focusedProfile/)
assert.match(pluginSource, /active && 'bg-\(--ui-row-active-background\)'/)
assert.match(pluginSource, /active: groupChatName === row\.name/)
})
test('disband: removes only this membership, room log, workspace, and needs-you state', async () => {
const gc = load(() => '(pass)')
@@ -42,6 +42,7 @@ function renderBotRow(input = 'alpha') {
$botMeta: atom({}),
$botUnread: atom({}),
$focusedBotProfile: atom('default'),
$groupChatWorkspace: atom(null),
$lastRoster: atom([]),
$selectedBot: atom('default'),
botAppearance: () => ({ shape: 'round', color: '#000', image: null }),
@@ -170,6 +171,7 @@ test('behavior: remote default does not open this-device chat when the source di
$botMeta: atom({ default: { chat: 'this-device-chat' } }),
$botUnread: atom({}),
$focusedBotProfile: atom('default'),
$groupChatWorkspace: atom(null),
$lastRoster: atom([]),
$selectedBot: atom('default'),
botAppearance: () => ({ shape: 'round', color: '#000', image: null }),