Skip to content

Commit e2a4a41

Browse files
committed
fix(social): one leave line when a face room closes (#1103)
Rebased onto 28c55ab (#1107 and #1108 rewrote the same 0008 hunk). patches/ds-bot/0008 is that tree plus this change only.
1 parent 28c55ab commit e2a4a41

13 files changed

Lines changed: 289 additions & 28 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
When the other side leaves a face room and that leave closes the room, the room says so once: the "left the room. Room closed." banner is the only line, so the short platform leave no longer appears next to it. A leave that leaves the room open still posts its short line ([#1103](https://github.com/ThinkFlowLab/FlyBot/issues/1103)).

‎core/client.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5631,6 +5631,18 @@ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId,
56315631
}
56325632
var WAITING_OTHERS_NOTICE = "Waiting for the owner's approval";
56335633
var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"];
5634+
function leaveNoticeCoveredByClose(room2, text) {
5635+
if (room2?.origin !== "face-to-face" || room2.face?.status !== "closed") return false;
5636+
const match = String(text ?? "").trim().match(/^(.+) left(?: the room)?$/);
5637+
if (!match) return false;
5638+
const who = match[1];
5639+
for (const participant of room2.participants ?? []) {
5640+
if (participant?.remote !== true || participant.leftSeq === void 0) continue;
5641+
const name = String(participant.ownerName || participant.displayName || participant.name || "").trim();
5642+
if (name && (who === name || who.endsWith(` (${name})`))) return true;
5643+
}
5644+
return false;
5645+
}
56345646
function useDismiss(open, close, refs) {
56355647
(0, import_react8.useEffect)(() => {
56365648
if (!open) return void 0;
@@ -6501,6 +6513,7 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi
65016513
return (0, import_react8.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name }));
65026514
}
65036515
if (reply.kind === "notice" && room2?.origin === "face-to-face" && String(reply.text ?? "").trim() === WAITING_OTHERS_NOTICE) return null;
6516+
if ((reply.kind === "notice" || reply.kind === "left") && leaveNoticeCoveredByClose(room2, reply.text)) return null;
65046517
if (reply.kind === "notice" || reply.kind === "joined" || reply.kind === "left" || reply.kind === "started") {
65056518
return (0, import_react8.createElement)("div", { key: `notice-${index}`, className: "bt-event", "data-flybot": "face-notice" }, remoteMessageText(reply.text));
65066519
}

‎core/src/client/cells.js‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,22 @@ export function Bubbles({ texts, roster, selfId, actions, streaming, sessionId,
5858
const WAITING_OTHERS_NOTICE = "Waiting for the owner's approval"
5959
const REACTIONS = ['👍', '❤️', '😂', '😮', '😢', '🙏']
6060

61+
// The closed status line already says who left (#1103). Same rule as
62+
// plugins/social/src/face-rooms.js leaveNoticeCoveredByClose: the plugin does not post the short
63+
// line when it already knows the room closed, and a line posted earlier is not drawn beside the banner.
64+
function leaveNoticeCoveredByClose(room, text) {
65+
if (room?.origin !== 'face-to-face' || room.face?.status !== 'closed') return false
66+
const match = String(text ?? '').trim().match(/^(.+) left(?: the room)?$/)
67+
if (!match) return false
68+
const who = match[1]
69+
for (const participant of room.participants ?? []) {
70+
if (participant?.remote !== true || participant.leftSeq === undefined) continue
71+
const name = String(participant.ownerName || participant.displayName || participant.name || '').trim()
72+
if (name && (who === name || who.endsWith(` (${name})`))) return true
73+
}
74+
return false
75+
}
76+
6177
// Closes a popover on a press outside the given elements or on Escape.
6278
function useDismiss(open, close, refs) {
6379
useEffect(() => {
@@ -920,6 +936,9 @@ export function GroupReplies({ sessionId, callId, done, content, roster, actions
920936
return h('div', { key: `${reply.botId}-${index}`, className: 'bt-event' }, t("You stopped {name}'s work", { name: bot.name }))
921937
}
922938
if (reply.kind === 'notice' && room?.origin === 'face-to-face' && String(reply.text ?? '').trim() === WAITING_OTHERS_NOTICE) return null
939+
// The closed status line already names this person (#1103). Same rule as the social plugin's
940+
// leaveNoticeCoveredByClose: a short "Name left the room" beside "Name left the room. Room closed."
941+
if ((reply.kind === 'notice' || reply.kind === 'left') && leaveNoticeCoveredByClose(room, reply.text)) return null
923942
if (reply.kind === 'notice' || reply.kind === 'joined' || reply.kind === 'left' || reply.kind === 'started') {
924943
return h('div', { key: `notice-${index}`, className: 'bt-event', 'data-flybot': 'face-notice' }, remoteMessageText(reply.text))
925944
}

‎core/test/tool-cells.test.js‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,40 @@ test('#278: a Face-to-face room drops the old "Waiting for the owner\'s approval
370370
assert.match(shown(root, { sessionId: 'room' }), /Waiting for the owner/)
371371
})
372372

373+
// #1103: the closed banner already says who left. A short leave notice beside it is the second line.
374+
test('#1103: a closed face room draws the short leave only when the banner does not cover it', () => {
375+
const replies = [
376+
{ kind: 'notice', text: 'SelfB1025 left the room' },
377+
{ kind: 'notice', text: 'Coder (SelfB1025) left' },
378+
{ kind: 'notice', text: 'Carol left the room' },
379+
{ kind: 'notice', text: 'Bob joined the room' },
380+
{ kind: 'notice', text: 'The room is now open. The number is no longer valid.' },
381+
]
382+
const closed = {
383+
...room, origin: 'face-to-face', face: { status: 'closed' },
384+
participants: [
385+
{ remote: false, displayName: 'Chief', ownerName: 'SelfA1025' },
386+
{ remote: true, displayName: 'Coder', ownerName: 'SelfB1025', leftSeq: 4 },
387+
],
388+
}
389+
const root = call('group_relay', 'relay', { content: { replies } })
390+
const drawn = shown(root, { sessionId: 'room', roster: team({ rooms: [closed], roomsById: { room: closed } }) })
391+
assert.doesNotMatch(drawn, /SelfB1025 left the room/)
392+
assert.doesNotMatch(drawn, /Coder \(SelfB1025\) left/)
393+
assert.match(drawn, /Carol left the room/)
394+
assert.match(drawn, /Bob joined the room/)
395+
assert.match(drawn, /The room is now open/)
396+
// The room is still open: the short line is the only leave signal.
397+
const open = { ...closed, face: { status: 'open' } }
398+
const live = shown(root, { sessionId: 'room', roster: team({ rooms: [open], roomsById: { room: open } }) })
399+
assert.match(live, /SelfB1025 left the room/)
400+
assert.match(live, /Coder \(SelfB1025\) left/)
401+
// Closed, but nobody recorded as gone: the banner cannot name them, so the short line stays.
402+
const unnamed = { ...closed, participants: [{ remote: false, displayName: 'Chief' }] }
403+
const bare = shown(root, { sessionId: 'room', roster: team({ rooms: [unnamed], roomsById: { room: unnamed } }) })
404+
assert.match(bare, /SelfB1025 left the room/)
405+
})
406+
373407
// The Secrets page says who may use a key with three explicit choices (#545): all my Bots,
374408
// chosen Bots, or none; a key for no Bot shows that choice, not a list with nobody in it.
375409
test('the scope editor offers all my Bots, chosen Bots, or none, and shows the current one', () => {

‎docs/subsystems/core-client.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ The overlay (`flybot-slots.js` `FlyBotApprovals`) is the fallback, not a second
179179
- Slot `flybot.new-chat.action` (`0003-flybot-api-client`, types in `flybot.d.ts`) is a row on the sidebar **+** new-chat menu, after New group chat. Plugins register `{ id, order, label, hint?, visible?, run }`; the picker looks `label` / `hint` up with `t()` like the neighbouring rows. A string returned from `run` is shown as a toast (`actions.notify`). Social registers Face-to-face room and Add friend; see [plugin-runtime](plugin-runtime.md) and [social](social.md).
180180
- Sidebar **+** (`mode === 'new'`) opens a short action menu (`data-menu`, no To:/composer) until **New chat** or **New group chat**. That menu does not list individual Bots (Chief and the rest stay behind **New chat**). The Agent row in that FlyBot menu is **Add coding agent**; every row has its own mark (`ChatIcon`, `BotIcon`, `PeopleIcon`, a plugin row's `icon`, drawn through `SlotIcon` behind its own error boundary with the people mark as fallback; a function or memo/forwardRef component, `TerminalIcon`), one verb style (New … / Add …) and one short hint with no closing period (#385: *Message one of your Bots*, *Set up a Bot with its own role*, *Chat with several of your Bots*, *Meet people nearby with a 6-digit number*, *Paste an invite link or code*, *A coding session without a Bot*). A Bot's right-click menu has **Edit Bot** and no separate **Rename Bot** (the name is edited there); **Add DSH Agent** stays the DSH product string on Agent rows elsewhere (#126, #181).
181181
- Group bubbles, notices, and failed-reply lines never show a raw `<untrusted-message>` wrap or a developer error (`llm-deepseek: no API key`). `remoteMessageText` unwraps stored fences; `failureLabel` maps missing-key / unknown model failures to a short friendly line. Details stay in Settings → Feedback.
182+
- A closed face room's status line already says who left (#1103). `GroupReplies` (`leaveNoticeCoveredByClose` in `cells.js`) does not also draw a short leave notice for that person (`SelfB1025 left the room`, or `Coder (SelfB1025) left`). A leave while the room is still open, a leave the status line does not name, and every other notice stay. The social plugin uses the same rule when it decides whether to post the line. Test: `core/test/tool-cells.test.js`.
182183
- `GroupReplies` draws a `user` reply with `other: true` (another member's face-room line, #180) as `PersonMessage` (`cells.js`): the person's name, the person mark (`PersonMark` in `mark.js`) and a received `--bt-bubble` bubble, not a centred `bt-event` notice and not the sent `--bt-user` style; own lines keep the right-hand bubble (#229). `other` comes only from the host's person-line post (`hooks.js` `postHuman`), never from text. Another home member's line in a shared group (`UserMessageCell`, author stamped by the host) gets the same person mark and the received surface (`.bt-umsg-other .bt-ubody`). `PersonMark({ name })` draws the person's initial on a color picked from the name (`personColor`, the core default-color hash of `person:<name>`), so one person looks the same on every home and two people are told apart; without a name it is the plain silhouette (#182).
183184
- A person's line keeps its line breaks (#455): `.bt-person-msg .bt-bubble :is(p,li)` is `white-space: pre-line`, as the sender's own `pre-wrap` bubble, since Markdown would join single newlines. A trigger whose source has `shown: true` (a line a running round already drew, host #455) draws nothing. Covered by `core/test/face-people.test.js`.
184185
- A face-to-face room is person-first (#229, #457, owner decision 2026-10-20). `face-seats.js` (re-exported by `group-settings.js`) builds it from participant fields and the roster, never message text: `faceSeats` lists the live Bot seats (`{ left: true }` adds the ones that left, marked `left`), `faceSeatLabel` names each Bot `<Bot> (<person>)`, mine included (`Chief (SelfB)`, `Chief (SelfA)`; a Bot is never `(you)`), `faceBotLabel` gives a local Bot that name in a face room, and `facePeople` groups the seats by person (me once, first; a friend by home) with `facePersonLabel` putting `(you)` on me only. The overview and group settings **Members · N** count people and draw one row per person (`FacePersonRow`: each Bot that person brought, drawn with the look the chat and the sidebar use — the local roster record, or the friend's seat/chat look, stepped off a same-named local Bot — then the person's name and the Bot names; my Bots open their chat). A row with no Bot still draws the person silhouette, not a blank disc (#1089). Owner names that share an initial (`SelfA1025`, `SelfB1025`) are not the mark (#1104).

0 commit comments

Comments
 (0)