Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1054-inviter-accept.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
When someone accepts your invite, the sidebar says who joined and People with access lists them even if the grant list is still empty, with one step to ask them to share a Bot back ([#1054](https://github.com/ThinkFlowLab/FlyBot/issues/1054)).
4 changes: 4 additions & 0 deletions docs/subsystems/social.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ Everything in `plugins/social/` (entry `plugins/social/index.js` and any helpers

A local Bot's turn that is answering a friend's line and ends in error does not stay silent. The host sends one of two fixed sentences (`Could not reply: this Bot has no model set up yet.` / `Could not reply: this Bot could not answer right now.`, `turns.js` `remoteFailureNotice`) with `via: 'turn-failed'` and `meta.notice: 'turn-failed'`. `isTurnFailureNotice` allows only those exact strings, so the reply hold does not apply and the sender's DM is not left on "Weighing the options…". The same words on `auto-reply`, or any other text on `turn-failed`, are a normal held reply. Bot Social's own blanket hold on that send is cleared the way an Auto-sent line is (`autoSendRemoteLine`, `holdIsBlanket` only); a finding of its own keeps its card. The sender's home shows the sentence in that friend's chat, which ends the weighing row. A notice that comes back as the next line does not earn another.

### Inviter sees an accepted invite (#1054)

Accepting an invite stays one-way: the joiner sees the inviter's Bot, and the inviter does not get a friend row for the joiner's Bot while `list_remote_bots` marks it `reply_only`. `relationship.changed` (`status: active`) that names a full grant of this member's Bot (`reply_only` not true, `granted_bot` is that Bot) is kept as one sidebar notice per Bot and relationship — "{who} accepted your invite. They can message {bot}. You cannot message them until they share a Bot with you." — on `social/status` as `acceptNotices`. The first status read seeds the ids already on screen, and only an id that appears after that is a toast with the same sentence, so a reload does not toast again. **Ask them to share a Bot** (`social/ask-share-back`) calls `send_as_member` with a fixed line; Bot Social answers `bot_not_shared`, stores nothing, and the joiner gets the existing `share.requested` card. A remote that is already `reply_only: false` is not sent that line. Nothing is granted from the notice. **Dismiss** (`social/accept-notice-dismiss`) drops it. People with access lists that joiner from the notice when `list_bot_grants` omits the grant, including after a reload, and does not list them twice when the grant list already has the same Bot. A `reply_only: false` remote (they shared a Bot back) is a friend row even though the outgoing grant still names them, and the notice closes. A `reply_only` grant, or a grant of a Bot that is not this member, does not open a notice. An event with no `grants` does not clear notices already kept.

### Paused friend chat (#404; no turn limit since 2026-10-19)

Owner decision 2026-10-19 18:05: friend chats have no turn limit and never need Extend. Bot Social (#28) dropped the turn budget, and its loop detection checks only Bot-written lines: a line the owner typed (`typed_by_owner`) is never refused for a loop. A send Bot Social still refuses with `conversation_paused` (a Bot repeating itself, an emergency stop, or a server that predates #28) marks that friend's chat paused (in memory, by the friend Bot's handle; `social/paused`). The friend's chat then shows a quiet **Chat paused** line in the session dock (`social-paused`, `PausedChat`): *Bot Social paused the last chat with this Bot (reason). Send your message again: it starts a new chat.* It has no buttons and no turn count. It clears on the next successful send to that Bot. FlyBot never calls `extend_conversation`, and there is no model tool or endpoint for it. The earlier Extend card was removed: re-opening a loop-paused chat let the owner's next identical line trip loop detection again at once, so Extend reported success and the send still failed.
Expand Down
51 changes: 48 additions & 3 deletions plugins/social/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2787,12 +2787,30 @@ window.__ModuleLoader__.load({
const [adding, setAdding] = useState(false)
const [faceOpen, setFaceOpen] = useState(false)
const [shareRequests, setShareRequests] = useState([])
const keepShareRequests = value => setShareRequests(Array.isArray(value?.shareRequests) ? value.shareRequests : [])
const [acceptNotices, setAcceptNotices] = useState([])
// null until the first status answer: a reload must not toast notices that were already there (#1054).
const seenAccept = useRef(null)
const applyStatus = (value) => {
setShareRequests(Array.isArray(value?.shareRequests) ? value.shareRequests : [])
const next = Array.isArray(value?.acceptNotices) ? value.acceptNotices : []
setAcceptNotices(next)
if (seenAccept.current === null) {
seenAccept.current = new Set(next.map(item => item?.id).filter(Boolean))
return
}
for (const item of next) {
if (!item?.id || seenAccept.current.has(item.id)) continue
seenAccept.current.add(item.id)
if (typeof actions?.notify === 'function') actions.notify(acceptNoticeSentence(item))
}
}
const loadApprovals = () => call('social/status', {}).then((value) => {
setError('')
keepShareRequests(value)
applyStatus(value)
}, e => setError(errorText(e)))
const dismissShareRequest = item => call('social/share-request-dismiss', { id: item.id }).then(keepShareRequests, e => setError(errorText(e)))
const dismissShareRequest = item => call('social/share-request-dismiss', { id: item.id }).then(applyStatus, e => setError(errorText(e)))
const dismissAccept = item => call('social/accept-notice-dismiss', { id: item.id }).then(applyStatus, e => setError(errorText(e)))
const askShareBack = item => call('social/ask-share-back', { botId: item.botId, to: item.from, noticeId: item.id }).then(applyStatus, e => setError(errorText(e)))
useEffect(() => {
loadApprovals()
if (typeof setInterval !== 'function') return undefined
Expand All @@ -2813,12 +2831,37 @@ window.__ModuleLoader__.load({
}
return h('section', { className: 'fs bt-social', 'aria-label': 'Social' },
error ? h('div', { className: 'fs-status', 'data-tone': 'error', style: { padding: '4px 10px' } }, error) : null,
h(AcceptNotices, { items: acceptNotices, onAsk: askShareBack, onDismiss: dismissAccept }),
h(ShareRequests, { items: shareRequests, actions, onDismiss: dismissShareRequest }),
h(FaceRooms, { actions, enabled: true, roster, remoteChats }),
adding ? h(AddFriend, { actions, remoteChats, onChange: loadApprovals, onDone: () => setAdding(false) }) : null,
faceOpen ? h(FaceToFaceGroup, { actions, roster, remoteChats, onClose: () => setFaceOpen(false) }) : null)
}

// Someone accepted an invite to one of my Bots (#1054). One-way stays; this card is the inviter's
// feedback and the one step that asks them to share a Bot back. Asking does not grant anything.
function acceptNoticeSentence(item) {
const who = item?.owner ? `${item.name} (${item.owner})` : (item?.name || 'A friend')
const bot = item?.botName || 'your Bot'
return `${who} accepted your invite. They can message ${bot}. You cannot message them until they share a Bot with you.`
}
function AcceptNotices({ items, onAsk, onDismiss } = {}) {
if (!Array.isArray(items) || items.length === 0) return null
return h('div', { className: 'fs-share-requests', 'data-flybot': 'accept-notices' },
items.map((item) => h('div', { key: item.id, className: 'fs-card fs-share-request', role: 'status', 'data-flybot': 'accept-notice' },
h('div', { className: 'fs-hint', 'data-flybot': 'accept-notice-text' }, acceptNoticeSentence(item)),
item.asked ? h('div', { className: 'fs-hint', 'data-flybot': 'accept-notice-asked' }, 'You asked them to share a Bot.') : null,
h('div', { className: 'fs-line' },
h('button', { type: 'button', className: 'fs-btn fs-btn-sm', 'data-flybot': 'accept-notice-dismiss', onClick: () => onDismiss?.(item) }, 'Dismiss'),
h('button', {
type: 'button',
className: 'fs-btn fs-btn-sm fs-btn-primary',
'data-flybot': 'accept-notice-ask',
disabled: item.asked === true,
onClick: () => { if (!item.asked) onAsk?.(item) },
}, 'Ask them to share a Bot')))))
}

// "Share this Bot?" (#336): a friend's Bot tried to start a chat with one of my Bots that is not
// shared with them, and Bot Social refused it. Buttons only (#349): Share opens that Bot's Sharing,
// where the owner decides; Dismiss drops the card. Nothing is shared from here.
Expand Down Expand Up @@ -3698,6 +3741,8 @@ window.__ModuleLoader__.load({
BotSocialDetails,
SocialServerSettings,
AddFriend,
AcceptNotices,
FriendsBots,
ShareRequests,
PendingApprovals,
ChatApproval,
Expand Down
Loading
Loading