Skip to content

feat(social): face-to-face group: join a room by entering the same 6-digit number (#83) - #88

Merged
Birfy merged 4 commits into
ThinkFlowLab:mainfrom
Birfy:cursor/face-to-face-group-35e7
Oct 15, 2026
Merged

Birfy merged 4 commits into
ThinkFlowLab:mainfrom
Birfy:cursor/face-to-face-group-35e7

Conversation

@Birfy

@Birfy Birfy commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

Closes #83. Server side: Birfy/bot-social#2 (docs/FACE_ROOMS.md is the API contract this client is built against).

What it does

  • Friends' Bots: 'Join with a number'. Enter 6 digits, pick which of your Bots to bring, and you join directly (no preview, no confirmation).
  • The room appears immediately with the full member list as ' · 's Bot', one-click Leave, 'Start chat' (voids the number) and the 10-minute countdown while it is a candidate.
  • Events (member.joined / member.left / room.started / room.message) render as system and attributed messages. Peer text is untrusted: sanitized and fenced wherever it can become model-visible.
  • Bot replies go through the server; held replies appear in the existing pending-approval UI. A visible one-click 'auto-send' toggle in the room (and from the pending state) maps to the server flag auto_approve_face_rooms.
  • Every server error code has a friendly message (rate_limited / locked_out with retry-after, room_full, code_void, invalid_code ...).
  • 'Published to friends' toggle in Bot settings (next to 'Invite to this Bot'), using set-published from Published Bots: only published Bots are reachable from outside (#51) #87, plus a published hint when joining.

Checks (head cd383ab)

Run: plugins/social 58, core 675, build-client --check, lint, plugins/tree 54, ds-bot-patches check, lint-plugins.
Not run: smoke bench, live DSH/Desktop, real Bot Social, browser pass.

Known gap

An unpublished Bot that never had a Bot Social identity cannot join (social only registers published Bots, #87), contradicting the issue's 'unpublished Bots may join this one room'. Needs a follow-up decision.

cursoragent and others added 4 commits October 15, 2026 10:36
Friends' Bots gets Join with a number: type six digits, pick which Bot
to bring (default first), and join with no preview. The matching room is
hosted as a normal native room (origin face-to-face, reusing ThinkFlowLab#82
participants, owner badges, and the membership window). Leave is one
click; Start chat voids the number. Member events and peer messages
poll Bot Social with the event cursor; peer text is fenced. Bot replies
POST /v1/face-rooms/{id}/messages and pending_approval uses the existing
Approve / Reject queue. FlyBot members join unpublished (ISSUE ThinkFlowLab#51 hook).

Closes ThinkFlowLab#83

Co-authored-by: Danyang Chen <birfychen@gmail.com>
The room header and pending-approval queue get Auto-send my Bot's
room messages, mapping to PATCH /v1/me { auto_approve_face_rooms }.
If Bot Social answers confirmation_required, the same click retries
with confirm_code so the UI never asks for a 6-character code.

Co-authored-by: Danyang Chen <birfychen@gmail.com>
…wLab#51)

Bot settings (next to Invite to this Bot) gets a Published to friends
checkbox that calls set-published { id, published }. Face-to-face join
shows whether the brought Bot is published, using flybot.bot.isPublished
and BotView.published from the merged ThinkFlowLab#51 core.

Co-authored-by: Danyang Chen <birfychen@gmail.com>
The auto-send controls in the group overview use t(), so they need
DICTIONARY.zh entries or the client i18n check fails.

Co-authored-by: Danyang Chen <birfychen@gmail.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T10:55:02.660090Z cd383ab PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd383ab33f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

} else {
const local = locals.some(bot => bot.handle && bot.handle === line.authorId)
if (local) continue
void flybot.rooms.postRemote?.(room.id, { handle: line.authorId, name: line.who, text: line.text })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Send the fenced peer body into face rooms

When a polled room.message comes from another owner, this posts line.text even though renderFaceEvent put the neutralized form in line.fenced (a repo-wide use search shows that field is otherwise only asserted in the test). postRemote embeds the raw value in the DSH group user message and transcript, so peer-controlled instructions or forged headers reach local Bots outside the promised <untrusted-message> boundary; pass the fenced value through the host path.

AGENTS.md reference: AGENTS.md:L18-L18

Useful? React with 👍 / 👎.

Comment on lines +929 to +932
const record = local
? faceLocalRecord(local, seq, at, member)
: faceRemoteRecord(member, seq, at)
room.participants.push(record)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Allow signed-in owners to speak in face rooms

In a member-auth deployment, face-room snapshots create only local- and remote-bot participant records. The normal composer passes the signed-in owner as a human actor, and postRoomMessage then requires participantOf(room, 'human', actor.memberId), so the visible owner gets You are not a member of that room and cannot kick off or respond in the newly opened group. Treat the owner of the joined local Bot as a permitted face-room speaker (or materialize an appropriate local human participant).

Useful? React with 👍 / 👎.

if (!room?.id) return undefined
settings.faceRooms[snapshot.roomId] = {
localRoomId: room.id,
cursor: Math.max(Number(settings.faceRooms[snapshot.roomId]?.cursor) || 0, Number(snapshot.eventCursor) || 0),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Acknowledge face events only after room delivery

When postRemote/postNotice rejects (for example, resolveAgent or session flush fails) or the host stops before that async work finishes, the snapshot cursor is already advanced, the event is put in seen, and both posting promises are discarded with void. The next GET uses this newer after cursor, so the remote message or notice is silently skipped forever; await delivery and commit the cursor/seen state only after it succeeds.

Useful? React with 👍 / 👎.

Comment on lines +107 to +110
room.origin === 'face-to-face' || members.length + remotes.length <= 2
? null
: h('button', { type: 'button', className: 'bt-x', 'aria-label': t('Remove {name} from the group', { name: remoteBotTitle(person) }), title: t('Remove from group'), onClick: () => update({ removeParticipants: [{ kind: 'bot', id: person.handle || person.id, remote: true }] }) }, '×'))),
room.origin === 'face-to-face' ? null : adding

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route face-room deletion through the leave endpoint

With the default face polling enabled, this face-specific settings branch hides member editing but leaves the generic Manage → Delete group action active. That action deletes only the core room; it never calls social/face-leave or removes the persisted face-room/server membership, so the next list poll sees the still-joined room and upsertFace recreates it as a new Session. Face-room deletion should invoke the leave flow or the generic delete control should be suppressed.

Useful? React with 👍 / 👎.

Comment on lines +174 to 177
const live = room.presentedParticipants ?? room.participants ?? []
const humans = live.filter(p => p.kind === 'human' && p.leftSeq === undefined)
const remotes = live.filter(p => p.remote && p.leftSeq === undefined)
const members = room.members.map(id => roster.byId[id]).filter(Boolean)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Show the presented owner label for local face Bots

For every native face-room overview, presentRoom supplies owner-qualified labels for local participants too, but this consumes live only for humans/remotes and rebuilds local members from the bare roster. Consequently the owner's Bot is shown as just Writer rather than Writer · Alice's Bot (or Alice's Chief), contradicting the full member-list contract; use the matching presented participant when rendering local members.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Face-to-face group: Bots join one room by entering the same 6-digit number

2 participants