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/498-friend-bot-name.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A friend's Bot now has one name everywhere in a friend chat, on both homes: the sidebar row, chat header, tab, composer placeholder and @ menu all read `Chief (SelfB1233)`, from one shared formatter, and the @ menu no longer shows the handle as the Bot's name. A held message's approval card names the person who gets it (`Chief → SelfB1233`), the same before and after a reload, instead of `Chief → Chief (SelfB1233)` ([#498](https://github.com/ThinkFlowLab/FlyBot/issues/498)).
57 changes: 30 additions & 27 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1467,6 +1467,21 @@ function remoteBotTitle(record) {
function remoteOwnerBadge(record) {
return ownerNameOf(record) || cleanPeerName(record?.handle, "Friend");
}
function friendRecord(bot) {
const remote = bot?.remote && typeof bot.remote === "object" ? bot.remote : {};
return {
displayName: remote.displayName || remote.name || bot?.name,
ownerName: remote.ownerName || remote.owner || remote.homeName,
handle: remote.handle ?? bot?.handle,
isChief: remote.isChief
};
}
function friendBotTitle(bot) {
return remoteBotTitle(friendRecord(bot));
}
function friendBotOwner(bot) {
return remoteOwnerBadge(friendRecord(bot));
}
function sharedRoomTitle(inviterName) {
return `Shared room from ${cleanPeerName(inviterName, "a friend")}`;
}
Expand Down Expand Up @@ -2143,6 +2158,7 @@ function registry2() {
globalThis.__flybot__.BotMark ??= BotMark;
globalThis.__flybot__.BotAvatar ??= BotAvatar;
globalThis.__flybot__.lookOf ??= lookOf;
globalThis.__flybot__.names ??= { friendBotTitle, friendBotOwner };
return slots;
}
var createClientApi2 = (options = {}) => createClientApi({ call: call2, registry: registry2, ...options });
Expand Down Expand Up @@ -2258,12 +2274,7 @@ var chatOf = (approval, roster) => approval.request?.meta?.roomId ?? approval.re
function recipientName(approval, roster) {
const handle = handleOf(approval.request?.to);
const bot = friendChatOf(approval, roster);
if (bot) {
const owner = bot.remote.owner || bot.remote.homeName;
if (owner) return `${bot.name} (${owner})`;
return bot.name;
}
return approval.recipientName ?? approval.targetName ?? handle ?? String(approval.request?.to ?? "");
return approval.recipientName || (bot ? friendBotOwner(bot) : "") || approval.targetName || handle || String(approval.request?.to ?? "");
}
function FlyBotApprovals({ useRoster, actions } = {}) {
const entries2 = useSlot("flybot.approvals");
Expand Down Expand Up @@ -3214,7 +3225,6 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default}
.bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto}
.bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400}
.bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bt-row-owner{flex:0 100 auto;min-width:24px;font-size:13px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bt-row-remote{flex:none;display:inline-flex;align-items:center;color:var(--bt-ink-2)}
.bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
Expand Down Expand Up @@ -4245,12 +4255,7 @@ function sessionTabLabel(roster, sessionId) {
const bot = roster.byId?.[sessionId];
const room2 = roster.roomsById?.[sessionId];
if (bot?.remote) {
return remoteBotTitle({
displayName: bot.name,
ownerName: bot.remote.homeName || bot.remote.owner,
handle: bot.remote.handle,
isChief: bot.remote.isChief
});
return friendBotTitle(bot);
}
if (room2 && room2.origin === "face-to-face" && (room2.face?.status === "closed" || room2.face?.status === "left") && isAutoFaceTitle(room2.name)) return "Face-to-face · closed";
if (room2) return room2.displayName || (room2.sharedFrom ? sharedRoomTitle(room2.sharedFrom.inviterName) : room2.name) || "";
Expand Down Expand Up @@ -5413,8 +5418,8 @@ function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, use
}
function HeldReply({ held, roster, sessionId, actions, closing, children }) {
const bot = roster.byId[sessionId];
const chat = (roster.bots ?? []).find((item) => item.remote?.handle === held.to && isListedRemoteChat(item));
const toName = chat ? remoteBotTitle({ displayName: chat.name, ownerName: chat.remote.homeName || chat.remote.owner, handle: chat.remote.handle, isChief: chat.remote.isChief }) : held.toOwner || held.to || t("a friend");
const chat = (roster.bots ?? []).find((item) => item.remote?.handle === held.to);
const toName = cleanPeerName(held.toOwner) || (chat ? friendBotOwner(chat) : ownerNameOf({ handle: held.to })) || held.to || t("a friend");
return (0, import_react8.createElement)(
"div",
{ className: "bt-held-reply", "data-state": held.status, "data-held": held.status === "pending" ? "" : void 0 },
Expand Down Expand Up @@ -6988,11 +6993,9 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio
actions.openMenu({ kind: "bot", id, x: event.clientX, y: event.clientY });
};
const pinned = bot.pinned === true && !tile;
const remoteRecord2 = bot.remote ? { displayName: bot.name, ownerName: bot.remote.homeName || bot.remote.owner, handle: bot.remote.handle, isChief: bot.remote.isChief } : null;
const shown = remoteRecord2 ? remoteBotTitle(remoteRecord2) : bot.name;
const rowName = remoteRecord2 ? cleanPeerName(bot.name, shown) : shown;
const owner = remoteRecord2 ? ownerNameOf(remoteRecord2) || cleanPeerName(bot.remote.handle) : "";
const secondLine = awayLine || preview || (remoteRecord2 ? t("Friend's Bot") : "");
const remote = Boolean(bot.remote);
const shown = remote ? friendBotTitle(bot) : bot.name;
const secondLine = awayLine || preview || (remote ? t("Friend's Bot") : "");
const label = [shown, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", waiting ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", ");
if (tile && wide) {
return (0, import_react12.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react12.createElement)(
Expand Down Expand Up @@ -7024,7 +7027,7 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio
"aria-current": active ? "page" : void 0,
// An own Bot's hover text names its model (#366); a friend's Bot runs on its owner's model.
"data-bot-id": id,
title: remoteRecord2 ? shown : botRowTitle(shown, modelOf(bot, roster)?.name, wide),
title: remote ? shown : botRowTitle(shown, modelOf(bot, roster)?.name, wide),
onClick: () => actions.openSession(id),
onContextMenu
},
Expand All @@ -7036,8 +7039,7 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio
(0, import_react12.createElement)(
"span",
{ className: "bt-row-name" },
(0, import_react12.createElement)("span", { className: "bt-row-title" }, rowName),
owner ? (0, import_react12.createElement)("span", { className: "bt-row-owner" }, owner) : null,
(0, import_react12.createElement)("span", { className: "bt-row-title" }, shown),
bot.remote ? (0, import_react12.createElement)(RemoteMark, { remote: bot.remote }) : null,
pinned ? (0, import_react12.createElement)(PinMark) : null
),
Expand Down Expand Up @@ -7908,7 +7910,7 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe
const bot = roster.byId[sessionId];
const room2 = roster.roomsById[sessionId];
const detailsOpen = useUi((state) => state.details === sessionId);
const name = bot?.remote ? remoteBotTitle({ displayName: bot.name, ownerName: bot.remote.homeName || bot.remote.owner, handle: bot.remote.handle, isChief: bot.remote.isChief }) : room2?.displayName || (room2?.sharedFrom ? sharedRoomTitle(room2.sharedFrom.inviterName) : room2?.name) || bot?.name || "";
const name = bot?.remote ? friendBotTitle(bot) : room2?.displayName || (room2?.sharedFrom ? sharedRoomTitle(room2.sharedFrom.inviterName) : room2?.name) || bot?.name || "";
const useNews = useMemoryNews ?? noNews;
const memoryIds = bot ? [bot.id] : room2?.members ?? [];
const notice = useMemoryNotice(memoryIds, useNews);
Expand Down Expand Up @@ -8408,8 +8410,9 @@ function atSources({ roster, sessions, launched, iconFor }) {
const snapshot = roster.getSnapshot();
const face = isFaceRoom(snapshot.roomsById?.[session.sessionId]);
const team = (mentionRows(snapshot, session.sessionId, query) ?? []).map((row) => ({
// A friend's Bot by its one name, "Chief (Bob)"; its handle is the second line (#384).
name: row.bot?.remote && !face ? remoteBotTitle(remoteRecord(row.bot)) : row.bot ? row.bot.label ?? row.bot.name : row.remote ? remoteBotTitle({ ...row.remote, displayName: row.remote.name, ownerName: row.remote.owner || row.remote.homeName }) : t("Everyone"),
// A friend's Bot by its one name, "Chief (Bob)"; its handle is the second line (#384). A seat
// carries its own name; a friend's chat is named as its sidebar row and header (#498).
name: row.bot?.remote && !face ? row.bot.kind === "bot" ? remoteBotTitle(remoteRecord(row.bot)) : friendBotTitle(row.bot) : row.bot ? row.bot.label ?? row.bot.name : row.remote ? remoteBotTitle({ ...row.remote, displayName: row.remote.name, ownerName: row.remote.owner || row.remote.homeName }) : t("Everyone"),
...row.bot?.role && !face ? { description: row.bot.role } : {},
...row.remote ? { description: row.remote.handle } : {},
...row.bot?.remote && !face && remoteRecord(row.bot).handle ? { description: remoteRecord(row.bot).handle } : {},
Expand Down Expand Up @@ -16627,7 +16630,7 @@ function mentionableBots(roster, sessionId, { fromFriend = false } = {}) {
const bots = room2 ? room2.members.map((id) => roster.byId[id]).filter(Boolean) : roster.byId[sessionId] ? roster.bots : [];
const humans = (room2?.participants ?? []).filter((p) => p.kind === "human" && p.leftSeq === void 0).map((p) => ({ id: p.id, name: p.displayName || p.id, kind: "human", role: p.role }));
const remotes = (room2?.participants ?? []).filter((p) => p.remote && p.leftSeq === void 0).map((p) => ({ id: p.handle || p.id, name: remoteBotTitle(p), kind: "bot", remote: p, mentionLabel: remoteBotTitle(p) }));
return bots.concat(humans, remotes, bots.filter((bot) => typeof bot?.remote?.handle === "string").map((bot) => ({ ...bot, name: bot.remote.handle.replace(/^@+/, ""), mentionLabel: remoteBotTitle({ displayName: bot.name, ownerName: bot.remote.homeName || bot.remote.owner, handle: bot.remote.handle, isChief: bot.remote.isChief }) })));
return bots.concat(humans, remotes, bots.filter((bot) => typeof bot?.remote?.handle === "string").map((bot) => ({ ...bot, name: bot.remote.handle.replace(/^@+/, ""), mentionLabel: friendBotTitle(bot) })));
}
function faceMentionables(room2, roster, fromFriend) {
const humans = (room2.participants ?? []).filter((p) => p.kind === "human" && p.leftSeq === void 0).map((p) => ({ id: p.id, name: p.displayName || p.id, kind: "human", role: p.role }));
Expand Down
7 changes: 4 additions & 3 deletions core/src/client/at-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// rows stay beside them, because a source cannot be hidden for one Session (a face
// room filters the session rows out of view: face-at-filter.js).
import { mentionCandidates, remoteMentionRows } from './flybot-registry.js'
import { cleanPeerName, ownerNameOf, remoteBotTitle, listedChatBots } from '../shared/remote-label.js'
import { cleanPeerName, friendBotTitle, ownerNameOf, remoteBotTitle, listedChatBots } from '../shared/remote-label.js'
import { t } from './i18n.js'
import { currentPart, relativeShort } from './text.js'

Expand Down Expand Up @@ -138,8 +138,9 @@ export function atSources({ roster, sessions, launched, iconFor }) {
const snapshot = roster.getSnapshot()
const face = isFaceRoom(snapshot.roomsById?.[session.sessionId])
const team = (mentionRows(snapshot, session.sessionId, query) ?? []).map(row => ({
// A friend's Bot by its one name, "Chief (Bob)"; its handle is the second line (#384).
name: row.bot?.remote && !face ? remoteBotTitle(remoteRecord(row.bot)) : row.bot ? (row.bot.label ?? row.bot.name) : row.remote ? remoteBotTitle({ ...row.remote, displayName: row.remote.name, ownerName: row.remote.owner || row.remote.homeName }) : t('Everyone'),
// A friend's Bot by its one name, "Chief (Bob)"; its handle is the second line (#384). A seat
// carries its own name; a friend's chat is named as its sidebar row and header (#498).
name: row.bot?.remote && !face ? (row.bot.kind === 'bot' ? remoteBotTitle(remoteRecord(row.bot)) : friendBotTitle(row.bot)) : row.bot ? (row.bot.label ?? row.bot.name) : row.remote ? remoteBotTitle({ ...row.remote, displayName: row.remote.name, ownerName: row.remote.owner || row.remote.homeName }) : t('Everyone'),
...(row.bot?.role && !face ? { description: row.bot.role } : {}),
...(row.remote ? { description: row.remote.handle } : {}),
...(row.bot?.remote && !face && remoteRecord(row.bot).handle ? { description: remoteRecord(row.bot).handle } : {}),
Expand Down
9 changes: 2 additions & 7 deletions core/src/client/brand.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Paths stay here so tests can import them without React.

import { isAutoFaceTitle } from '../shared/face-title.js'
import { remoteBotTitle, sharedRoomTitle } from '../shared/remote-label.js'
import { friendBotTitle, sharedRoomTitle } from '../shared/remote-label.js'

export const FLYBOT_WING = 'M150 238 C 96 200, 40 196, 22 214 C 8 230, 40 262, 150 300 Z'
export const FLYBOT_WING_LOW = 'M150 318 C 104 300, 62 306, 54 322 C 48 338, 84 352, 150 352 Z'
Expand Down Expand Up @@ -32,12 +32,7 @@ export function sessionTabLabel(roster, sessionId) {
const bot = roster.byId?.[sessionId]
const room = roster.roomsById?.[sessionId]
if (bot?.remote) {
return remoteBotTitle({
displayName: bot.name,
ownerName: bot.remote.homeName || bot.remote.owner,
handle: bot.remote.handle,
isChief: bot.remote.isChief,
})
return friendBotTitle(bot)
}
// An ended face-to-face room (closed, or someone left) no longer lists people who are not in it (#250).
// A name the user chose is kept.
Expand Down
11 changes: 5 additions & 6 deletions core/src/client/cells.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { AnsweredCell, SettledQuestion, PendingQuestion } from './questions.js'
import { SecretToolCell } from './secret-card.js'
import { dateLocale, t } from './i18n.js'
import { remoteMessageText } from './flybot-registry.js'
import { isListedRemoteChat, normRemoteHandle, remoteBotTitle } from '../shared/remote-label.js'
import { cleanPeerName, friendBotOwner, normRemoteHandle, ownerNameOf, remoteBotTitle } from '../shared/remote-label.js'
import { faceBotLabel, faceSeatLabel, faceSeats } from './face-seats.js'
import { fileSize } from './flybot-slots.js'
import { HeldReplyActions } from './held-reply.js'
Expand Down Expand Up @@ -193,11 +193,10 @@ export function AssistantCell({ node, groupPart, sessionId, openFile, fileMentio
// turn's closing text carries the buttons; an earlier text of the same turn is only marked.
function HeldReply({ held, roster, sessionId, actions, closing, children }) {
const bot = roster.byId[sessionId]
// The friend's Bot by name and owner when its chat is listed (#384), else the person (#418).
const chat = (roster.bots ?? []).find(item => item.remote?.handle === held.to && isListedRemoteChat(item))
const toName = chat
? remoteBotTitle({ displayName: chat.name, ownerName: chat.remote.homeName || chat.remote.owner, handle: chat.remote.handle, isChief: chat.remote.isChief })
: held.toOwner || held.to || t('a friend')
// The person who gets the reply (#418, #498): the host stored it with the hold, so the card reads
// the same live and after a reload; "Chief → Chief (SelfB1233)" named two Bots and no person.
const chat = (roster.bots ?? []).find(item => item.remote?.handle === held.to)
const toName = cleanPeerName(held.toOwner) || (chat ? friendBotOwner(chat) : ownerNameOf({ handle: held.to })) || held.to || t('a friend')
return h('div', { className: 'bt-held-reply', 'data-state': held.status, 'data-held': held.status === 'pending' ? '' : undefined },
children,
closing ? h(HeldReplyActions, { held: { ...held, toName, from: bot?.name }, actions }) : null)
Expand Down
17 changes: 8 additions & 9 deletions core/src/client/flybot-slots.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@
// Section(title) and Row(label, hint) so the page looks like the others and reads zh in a zh UI
//
// The same registry object also carries the host Bot look so a plugin can draw a Bot
// exactly as the sidebar does: __flybot__.BotAvatar / BotMark / lookOf (set in registry()).
// exactly as the sidebar does: __flybot__.BotAvatar / BotMark / lookOf, and name a friend's Bot
// or its owner the same way: __flybot__.names.friendBotTitle / friendBotOwner (set in registry()).
//
// Types: ds-bot/types/flybot.d.ts (FlyBotClientRegistry).
import { Component, createElement as h, Fragment, useEffect, useState } from 'react'
import { call as callHost } from './api.js'
import { isListedRemoteChat } from '../shared/remote-label.js'
import { friendBotOwner, friendBotTitle, isListedRemoteChat } from '../shared/remote-label.js'
import { registry as baseRegistry } from './flybot-registry.js'
import { BotAvatar, BotMark, lookOf } from './mark.js'
import { t } from './i18n.js'
Expand All @@ -52,6 +53,8 @@ export function registry() {
globalThis.__flybot__.BotMark ??= BotMark
globalThis.__flybot__.BotAvatar ??= BotAvatar
globalThis.__flybot__.lookOf ??= lookOf
// The same for a friend's Bot's one name and its owner (#498), so the social plugin says it as the sidebar does.
globalThis.__flybot__.names ??= { friendBotTitle, friendBotOwner }
return slots
}

Expand Down Expand Up @@ -182,16 +185,12 @@ const friendChatOf = (approval, roster) => {
// Bot that wrote it (#278). The chat's dock draws its card; the pointer opens that chat.
// A Bot's held reply to a friend is decided on its bubble in the chat it answers (#418).
const chatOf = (approval, roster) => approval.request?.meta?.roomId ?? approval.request?.meta?.replyIn?.sessionId ?? friendChatOf(approval, roster)?.id ?? (typeof approval.request?.from === 'string' ? approval.request.from : undefined)
// A friend's Bot by name and owner when the roster knows its chat; otherwise what the host called it.
// The person who gets it (#418, #498): the host's `recipientName`, else the owner of the friend's chat,
// else what the host called it. "Chief → SelfB1233", never two Bot names side by side.
function recipientName(approval, roster) {
const handle = handleOf(approval.request?.to)
const bot = friendChatOf(approval, roster)
if (bot) {
const owner = bot.remote.owner || bot.remote.homeName
if (owner) return `${bot.name} (${owner})`
return bot.name
}
return approval.recipientName ?? approval.targetName ?? handle ?? String(approval.request?.to ?? '')
return approval.recipientName || (bot ? friendBotOwner(bot) : '') || approval.targetName || handle || String(approval.request?.to ?? '')
}

export function FlyBotApprovals({ useRoster, actions } = {}) {
Expand Down
Loading
Loading